Merge pull request #35 from MaxMustermann2/validator-sigs

add `hex_to_one`, fix `bech32` installation, bug fix
pull/36/head
Max 2 years ago committed by GitHub
commit d12bde7328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      .gitignore
  2. 404
      .style.yapf
  3. 2
      Makefile
  4. 35
      README.md
  5. 21
      pyhmy/__init__.py
  6. 8
      pyhmy/_version.py
  7. 409
      pyhmy/account.py
  8. 0
      pyhmy/bech32/__init__.py
  9. 115
      pyhmy/bech32/bech32.py
  10. 988
      pyhmy/blockchain.py
  11. 414
      pyhmy/cli.py
  12. 18
      pyhmy/constants.py
  13. 209
      pyhmy/contract.py
  14. 64
      pyhmy/exceptions.py
  15. 169
      pyhmy/logging.py
  16. 30
      pyhmy/numbers.py
  17. 37
      pyhmy/rpc/exceptions.py
  18. 66
      pyhmy/rpc/request.py
  19. 262
      pyhmy/signing.py
  20. 575
      pyhmy/staking.py
  21. 530
      pyhmy/staking_signing.py
  22. 297
      pyhmy/staking_structures.py
  23. 539
      pyhmy/transaction.py
  24. 181
      pyhmy/util.py
  25. 694
      pyhmy/validator.py
  26. 1
      pytest.ini
  27. 2
      setup.cfg
  28. 578
      tests/GenerateRawTransactions.ipynb
  29. 10
      tests/bech32-pyhmy/test_bech32.py
  30. 61
      tests/cli-pyhmy/test_cli.py
  31. 20
      tests/logging-pyhmy/test_logging.py
  32. 41
      tests/numbers-pyhmy/test_numbers.py
  33. 83
      tests/request-pyhmy/test_request.py
  34. 348
      tests/sdk-pyhmy/conftest.py
  35. 219
      tests/sdk-pyhmy/test_account.py
  36. 628
      tests/sdk-pyhmy/test_blockchain.py
  37. 116
      tests/sdk-pyhmy/test_contract.py
  38. 56
      tests/sdk-pyhmy/test_signing.py
  39. 387
      tests/sdk-pyhmy/test_staking.py
  40. 86
      tests/sdk-pyhmy/test_staking_signing.py
  41. 394
      tests/sdk-pyhmy/test_transaction.py
  42. 244
      tests/sdk-pyhmy/test_validator.py
  43. 63
      tests/util-pyhmy/test_util.py

3
.gitignore vendored

@ -131,3 +131,6 @@ dmypy.json
# IDE # IDE
.idea .idea
# VIM
*.swp

@ -0,0 +1,404 @@
[style]
# Align closing bracket with visual indentation.
align_closing_bracket_with_visual_indent=True
# Allow dictionary keys to exist on multiple lines. For example:
#
# x = {
# ('this is the first element of a tuple',
# 'this is the second element of a tuple'):
# value,
# }
allow_multiline_dictionary_keys=False
# Allow lambdas to be formatted on more than one line.
allow_multiline_lambdas=False
# Allow splitting before a default / named assignment in an argument list.
allow_split_before_default_or_named_assigns=True
# Allow splits before the dictionary value.
allow_split_before_dict_value=False
# Let spacing indicate operator precedence. For example:
#
# a = 1 * 2 + 3 / 4
# b = 1 / 2 - 3 * 4
# c = (1 + 2) * (3 - 4)
# d = (1 - 2) / (3 + 4)
# e = 1 * 2 - 3
# f = 1 + 2 + 3 + 4
#
# will be formatted as follows to indicate precedence:
#
# a = 1*2 + 3/4
# b = 1/2 - 3*4
# c = (1+2) * (3-4)
# d = (1-2) / (3+4)
# e = 1*2 - 3
# f = 1 + 2 + 3 + 4
#
arithmetic_precedence_indication=False
# Number of blank lines surrounding top-level function and class
# definitions.
blank_lines_around_top_level_definition=2
# Number of blank lines between top-level imports and variable
# definitions.
blank_lines_between_top_level_imports_and_variables=1
# Insert a blank line before a class-level docstring.
blank_line_before_class_docstring=False
# Insert a blank line before a module docstring.
blank_line_before_module_docstring=False
# Insert a blank line before a 'def' or 'class' immediately nested
# within another 'def' or 'class'. For example:
#
# class Foo:
# # <------ this blank line
# def method():
# ...
blank_line_before_nested_class_or_def=False
# Do not split consecutive brackets. Only relevant when
# dedent_closing_brackets is set. For example:
#
# call_func_that_takes_a_dict(
# {
# 'key1': 'value1',
# 'key2': 'value2',
# }
# )
#
# would reformat to:
#
# call_func_that_takes_a_dict({
# 'key1': 'value1',
# 'key2': 'value2',
# })
coalesce_brackets=False
# The column limit.
column_limit=80
# The style for continuation alignment. Possible values are:
#
# - SPACE: Use spaces for continuation alignment. This is default behavior.
# - FIXED: Use fixed number (CONTINUATION_INDENT_WIDTH) of columns
# (ie: CONTINUATION_INDENT_WIDTH/INDENT_WIDTH tabs or
# CONTINUATION_INDENT_WIDTH spaces) for continuation alignment.
# - VALIGN-RIGHT: Vertically align continuation lines to multiple of
# INDENT_WIDTH columns. Slightly right (one tab or a few spaces) if
# cannot vertically align continuation lines with indent characters.
continuation_align_style=SPACE
# Indent width used for line continuations.
continuation_indent_width=4
# Put closing brackets on a separate line, dedented, if the bracketed
# expression can't fit in a single line. Applies to all kinds of brackets,
# including function definitions and calls. For example:
#
# config = {
# 'key1': 'value1',
# 'key2': 'value2',
# } # <--- this bracket is dedented and on a separate line
#
# time_series = self.remote_client.query_entity_counters(
# entity='dev3246.region1',
# key='dns.query_latency_tcp',
# transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
# start_ts=now()-timedelta(days=3),
# end_ts=now(),
# ) # <--- this bracket is dedented and on a separate line
dedent_closing_brackets=True
# Disable the heuristic which places each list element on a separate line
# if the list is comma-terminated.
disable_ending_comma_heuristic=True
# Place each dictionary entry onto its own line.
each_dict_entry_on_separate_line=True
# Require multiline dictionary even if it would normally fit on one line.
# For example:
#
# config = {
# 'key1': 'value1'
# }
force_multiline_dict=True
# The regex for an i18n comment. The presence of this comment stops
# reformatting of that line, because the comments are required to be
# next to the string they translate.
i18n_comment=#\..*
# The i18n function call names. The presence of this function stops
# reformattting on that line, because the string it has cannot be moved
# away from the i18n comment.
i18n_function_call=N_, _
# Indent blank lines.
indent_blank_lines=False
# Put closing brackets on a separate line, indented, if the bracketed
# expression can't fit in a single line. Applies to all kinds of brackets,
# including function definitions and calls. For example:
#
# config = {
# 'key1': 'value1',
# 'key2': 'value2',
# } # <--- this bracket is indented and on a separate line
#
# time_series = self.remote_client.query_entity_counters(
# entity='dev3246.region1',
# key='dns.query_latency_tcp',
# transform=Transformation.AVERAGE(window=timedelta(seconds=60)),
# start_ts=now()-timedelta(days=3),
# end_ts=now(),
# ) # <--- this bracket is indented and on a separate line
indent_closing_brackets=False
# Indent the dictionary value if it cannot fit on the same line as the
# dictionary key. For example:
#
# config = {
# 'key1':
# 'value1',
# 'key2': value1 +
# value2,
# }
indent_dictionary_value=False
# The number of columns to use for indentation.
indent_width=4
# Join short lines into one line. E.g., single line 'if' statements.
join_multiple_lines=False
# Do not include spaces around selected binary operators. For example:
#
# 1 + 2 * 3 - 4 / 5
#
# will be formatted as follows when configured with "*,/":
#
# 1 + 2*3 - 4/5
no_spaces_around_selected_binary_operators=
# Use spaces around default or named assigns.
spaces_around_default_or_named_assign=True
# Adds a space after the opening '{' and before the ending '}' dict
# delimiters.
#
# {1: 2}
#
# will be formatted as:
#
# { 1: 2 }
spaces_around_dict_delimiters=True
# Adds a space after the opening '[' and before the ending ']' list
# delimiters.
#
# [1, 2]
#
# will be formatted as:
#
# [ 1, 2 ]
spaces_around_list_delimiters=True
# Use spaces around the power operator.
spaces_around_power_operator=False
# Use spaces around the subscript / slice operator. For example:
#
# my_list[1 : 10 : 2]
spaces_around_subscript_colon=True
# Adds a space after the opening '(' and before the ending ')' tuple
# delimiters.
#
# (1, 2, 3)
#
# will be formatted as:
#
# ( 1, 2, 3 )
spaces_around_tuple_delimiters=True
# The number of spaces required before a trailing comment.
# This can be a single value (representing the number of spaces
# before each trailing comment) or list of values (representing
# alignment column values; trailing comments within a block will
# be aligned to the first column value that is greater than the maximum
# line length within the block). For example:
#
# With spaces_before_comment=5:
#
# 1 + 1 # Adding values
#
# will be formatted as:
#
# 1 + 1 # Adding values <-- 5 spaces between the end of the
# # statement and comment
#
# With spaces_before_comment=15, 20:
#
# 1 + 1 # Adding values
# two + two # More adding
#
# longer_statement # This is a longer statement
# short # This is a shorter statement
#
# a_very_long_statement_that_extends_beyond_the_final_column # Comment
# short # This is a shorter statement
#
# will be formatted as:
#
# 1 + 1 # Adding values <-- end of line comments in block
# # aligned to col 15
# two + two # More adding
#
# longer_statement # This is a longer statement <-- end of line
# # comments in block aligned to col 20
# short # This is a shorter statement
#
# a_very_long_statement_that_extends_beyond_the_final_column # Comment <-- the end of line comments are aligned based on the line length
# short # This is a shorter statement
#
spaces_before_comment=2
# Insert a space between the ending comma and closing bracket of a list,
# etc.
space_between_ending_comma_and_closing_bracket=True
# Use spaces inside brackets, braces, and parentheses. For example:
#
# method_call( 1 )
# my_dict[ 3 ][ 1 ][ get_index( *args, **kwargs ) ]
# my_set = { 1, 2, 3 }
space_inside_brackets=True
# Split before arguments
split_all_comma_separated_values=True
# Split before arguments, but do not split all subexpressions recursively
# (unless needed).
split_all_top_level_comma_separated_values=False
# Split before arguments if the argument list is terminated by a
# comma.
split_arguments_when_comma_terminated=True
# Set to True to prefer splitting before '+', '-', '*', '/', '//', or '@'
# rather than after.
split_before_arithmetic_operator=False
# Set to True to prefer splitting before '&', '|' or '^' rather than
# after.
split_before_bitwise_operator=False
# Split before the closing bracket if a list or dict literal doesn't fit on
# a single line.
split_before_closing_bracket=True
# Split before a dictionary or set generator (comp_for). For example, note
# the split before the 'for':
#
# foo = {
# variable: 'Hello world, have a nice day!'
# for variable in bar if variable != 42
# }
split_before_dict_set_generator=True
# Split before the '.' if we need to split a longer expression:
#
# foo = ('This is a really long string: {}, {}, {}, {}'.format(a, b, c, d))
#
# would reformat to something like:
#
# foo = ('This is a really long string: {}, {}, {}, {}'
# .format(a, b, c, d))
split_before_dot=True
# Split after the opening paren which surrounds an expression if it doesn't
# fit on a single line.
split_before_expression_after_opening_paren=False
# If an argument / parameter list is going to be split, then split before
# the first argument.
split_before_first_argument=False
# Set to True to prefer splitting before 'and' or 'or' rather than
# after.
split_before_logical_operator=False
# Split named assignments onto individual lines.
split_before_named_assigns=True
# Set to True to split list comprehensions and generators that have
# non-trivial expressions and multiple clauses before each of these
# clauses. For example:
#
# result = [
# a_long_var + 100 for a_long_var in xrange(1000)
# if a_long_var % 10]
#
# would reformat to something like:
#
# result = [
# a_long_var + 100
# for a_long_var in xrange(1000)
# if a_long_var % 10]
split_complex_comprehension=True
# The penalty for splitting right after the opening bracket.
split_penalty_after_opening_bracket=300
# The penalty for splitting the line after a unary operator.
split_penalty_after_unary_operator=10000
# The penalty of splitting the line around the '+', '-', '*', '/', '//',
# ``%``, and '@' operators.
split_penalty_arithmetic_operator=300
# The penalty for splitting right before an if expression.
split_penalty_before_if_expr=0
# The penalty of splitting the line around the '&', '|', and '^'
# operators.
split_penalty_bitwise_operator=300
# The penalty for splitting a list comprehension or generator
# expression.
split_penalty_comprehension=2100
# The penalty for characters over the column limit.
split_penalty_excess_character=7000
# The penalty incurred by adding a line split to the logical line. The
# more line splits added the higher the penalty.
split_penalty_for_added_line_split=30
# The penalty of splitting a list of "import as" names. For example:
#
# from a_very_long_or_indented_module_name_yada_yad import (long_argument_1,
# long_argument_2,
# long_argument_3)
#
# would reformat to something like:
#
# from a_very_long_or_indented_module_name_yada_yad import (
# long_argument_1, long_argument_2, long_argument_3)
split_penalty_import_names=0
# The penalty of splitting the line around the 'and' and 'or'
# operators.
split_penalty_logical_operator=300
# Use the Tab character for indentation.
use_tabs=False

@ -29,7 +29,7 @@ dev:
python3 -m pip install pytest-ordering python3 -m pip install pytest-ordering
test: test:
python3 -m py.test -r s -s tests python3 -m pytest -r s -s tests
install: install:
python3 -m pip install -e . python3 -m pip install -e .

@ -34,16 +34,16 @@ git clone https://github.com/harmony-one/mcl.git
git clone https://github.com/harmony-one/bls.git git clone https://github.com/harmony-one/bls.git
git clone https://github.com/harmony-one/harmony.git git clone https://github.com/harmony-one/harmony.git
cd harmony cd harmony
make test-rpc make debug
``` ```
Once the terminal displays `=== FINISHED RPC TESTS ===`, use another shell to run the following tests Once the terminal displays a couple of `Started server` lines, use another shell to run the following tests
```bash ```bash
make test make test
``` ```
Or directly with `pytest` (reference [here](https://docs.pytest.org/en/latest/index.html) for more info): Or directly with `pytest` (reference [here](https://docs.pytest.org/en/latest/index.html) for more info):
```bash ```bash
py.test tests pytest tests
``` ```
## Releasing ## Releasing
@ -61,6 +61,19 @@ test_address = 'one18t4yj4fuutj83uwqckkvxp9gfa0568uc48ggj7'
main_net = 'https://rpc.s0.t.hmny.io' main_net = 'https://rpc.s0.t.hmny.io'
main_net_shard_1 = 'https://rpc.s1.t.hmny.io' main_net_shard_1 = 'https://rpc.s1.t.hmny.io'
``` ```
#### utilities
##### Address conversion
```py
from pyhmy import util
hex_addr = util.convert_one_to_hex('one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3')
one_addr = util.convert_hex_to_one('0xA5241513DA9F4463F1d4874b548dFBAC29D91f34')
```
##### Ether / Wei conversion
```py
from pyhmy import numbers
one_ether_in_wei = numbers.convert_one_to_atto(1) # as a decimal.Decimal
wei_to_ether = numbers.convert_atto_to_one(int(1e18))
```
#### accounts #### accounts
```py ```py
from pyhmy import account from pyhmy import account
@ -245,7 +258,7 @@ delegator_addr = 'one1y2624lg0mpkxkcttaj0c85pp8pfmh2tt5zhdte'
```py ```py
all_validators = staking.get_all_validator_addresses(endpoint=test_net) # list of addresses all_validators = staking.get_all_validator_addresses(endpoint=test_net) # list of addresses
validator_information = staking.get_validator_information(validator_addr, endpoint=test_net) # dict with all info validator_information = staking.get_validator_information(validator_addr, endpoint=test_net) # dict with all info
validator_information_100 = staking.get_all_validator_information(page=0, endpoint=test_net) # for all use page=-1 validator_information_100 = staking.get_all_validator_information(page=0, endpoint=test_net)
elected_validators = staking.get_elected_validator_addresses(endpoint=test_net) # list of addresses elected_validators = staking.get_elected_validator_addresses(endpoint=test_net) # list of addresses
validators_for_epoch = staking.get_validators(epoch=73772, endpoint=test_net) # dict with list of validators and balance validators_for_epoch = staking.get_validators(epoch=73772, endpoint=test_net) # dict with list of validators and balance
validators_information_100_for_block = staking.get_all_validator_information_by_block_number(block_num=9017724, page=0, endpoint=test_net) validators_information_100_for_block = staking.get_all_validator_information_by_block_number(block_num=9017724, page=0, endpoint=test_net)
@ -256,7 +269,7 @@ total_delegation = staking.get_validator_total_delegation(validator_addr, endpoi
``` ```
##### Delegation ##### Delegation
```py ```py
delegation_information = staking.get_all_delegation_information(page=-1, endpoint=test_net) delegation_information = staking.get_all_delegation_information(page=0, endpoint=test_net)
delegations_by_delegator = staking.get_delegations_by_delegator(delegator_addr, test_net) delegations_by_delegator = staking.get_delegations_by_delegator(delegator_addr, test_net)
delegations_by_delegator_at_block = staking.get_delegations_by_delegator_by_block_number(delegator_addr, block_num=9017724, endpoint=test_net) delegations_by_delegator_at_block = staking.get_delegations_by_delegator_by_block_number(delegator_addr, block_num=9017724, endpoint=test_net)
delegation_by_delegator_and_validator = staking.get_delegation_by_delegator_and_validator(delegator_addr, validator_addr, test_net) delegation_by_delegator_and_validator = staking.get_delegation_by_delegator_and_validator(delegator_addr, validator_addr, test_net)
@ -295,7 +308,10 @@ info = {
'max-rate': '0.9', 'max-rate': '0.9',
'max-change-rate': '0.05', 'max-change-rate': '0.05',
'rate': '0.01', 'rate': '0.01',
'bls-public-keys': ['0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611'], 'bls-public-keys': ['0xa20e70089664a874b00251c5e85d35a73871531306f3af43e02138339d294e6bb9c4eb82162199c6a852afeaa8d68712'],
"bls-key-sigs": [
"0xef2c49a2f31fbbd23c21bc176eaf05cd0bebe6832033075d81fea7cff6f9bc1ab42f3b6895c5493fe645d8379d2eaa1413de55a9d3ce412a4f747cb57d52cc4da4754bfb2583ec9a41fe5dd48287f964f276336699959a5fcef3391dc24df00d",
]
'max-total-delegation': convert_one_to_atto(40000) 'max-total-delegation': convert_one_to_atto(40000)
} }
validator.load(info) validator.load(info)
@ -307,7 +323,7 @@ signed_create_tx_hash = validator.sign_create_validator_transaction(
gas_price = 1, gas_price = 1,
gas_limit = 100, gas_limit = 100,
private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48', private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48',
chain_id = None).rawTransaction.hex() chain_id = 2).rawTransaction.hex()
``` ```
To edit validator, change its parameters using the `setter` functions, for example, `validator.set_details`, except the `rate`, `bls_keys_to_add` and `bls_keys_to_remove` which can be passed to the below function: To edit validator, change its parameters using the `setter` functions, for example, `validator.set_details`, except the `rate`, `bls_keys_to_add` and `bls_keys_to_remove` which can be passed to the below function:
```py ```py
@ -316,8 +332,9 @@ signed_edit_tx_hash = validator.sign_edit_validator_transaction(
gas_price = 1, gas_price = 1,
gas_limit = 100, gas_limit = 100,
rate = '0.06', rate = '0.06',
bls_keys_to_add = "0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611", bls_key_to_add = "0xb8c3b3a0f1966c169ca73c348f4b8aee333a407125ab5c67f1d6e1e18ab052ed5fff0f1f7d4a7f789528b5ccd9c47b04",
bls_keys_to_remove = '0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608612', bls_key_to_add_sig = "0x3de4dff17451fb76a9690efce34bced97dd87eccd371fcd25335826cb879ca21281e82e5c2c76d4ef0ab0fc16e462312628834cbc1f29008b28e16a757367808be85180945b991be3103f98c14c7e3b3e54796d34aab4d8e812d440aa251c419",
bls_keys_to_remove = '0xa20e70089664a874b00251c5e85d35a73871531306f3af43e02138339d294e6bb9c4eb82162199c6a852afeaa8d68712',
private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48', private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48',
chain_id = 2).rawTransaction.hex() chain_id = 2).rawTransaction.hex()
``` ```

@ -1,18 +1,13 @@
"""
`pyhmy` for interacting with the Harmony blockchain
"""
import sys import sys
import warnings import warnings
from ._version import __version__ from ._version import __version__
from .util import (
Typgpy,
get_gopath,
get_goversion,
get_bls_build_variables,
json_load
)
if sys.version_info.major < 3: if sys.version_info.major < 3:
warnings.simplefilter("always", DeprecationWarning) warnings.simplefilter( "always", DeprecationWarning )
warnings.warn( warnings.warn(
DeprecationWarning( DeprecationWarning(
"`pyhmy` does not support Python 2. Please use Python 3." "`pyhmy` does not support Python 2. Please use Python 3."
@ -20,11 +15,9 @@ if sys.version_info.major < 3:
) )
warnings.resetwarnings() warnings.resetwarnings()
if sys.platform.startswith('win32') or sys.platform.startswith('cygwin'): if sys.platform.startswith( "win32" ) or sys.platform.startswith( "cygwin" ):
warnings.simplefilter("always", ImportWarning) warnings.simplefilter( "always", ImportWarning )
warnings.warn( warnings.warn(
ImportWarning( ImportWarning( "`pyhmy` does not work on Windows or Cygwin." )
"`pyhmy` does not work on Windows or Cygwin."
)
) )
warnings.resetwarnings() warnings.resetwarnings()

@ -1,11 +1,9 @@
""" """Provides pyhmy version information."""
Provides pyhmy version information.
"""
# This file is auto-generated! Do not edit! # This file is auto-generated! Do not edit!
# Use `python -m incremental.update pyhmy` to change this file. # Use `python -m incremental.update pyhmy` to change this file.
from incremental import Version from incremental import Version
__version__ = Version('pyhmy', 20, 5, 20) __version__ = Version( "pyhmy", 20, 5, 20 )
__all__ = ["__version__"] __all__ = [ "__version__" ]

@ -1,31 +1,20 @@
from .rpc.request import ( """
rpc_request Interact with accounts on the Harmony blockchain
) """
from .rpc.request import rpc_request
from .rpc.exceptions import ( from .rpc.exceptions import RPCError, RequestsError, RequestsTimeoutError
RPCError,
RequestsError,
RequestsTimeoutError
)
from .exceptions import ( from .exceptions import InvalidRPCReplyError
InvalidRPCReplyError
)
from .blockchain import ( from .blockchain import get_sharding_structure
get_sharding_structure
)
from .bech32.bech32 import ( from .bech32.bech32 import bech32_decode
bech32_decode
)
_default_endpoint = 'http://localhost:9500' from .constants import DEFAULT_ENDPOINT, DEFAULT_TIMEOUT
_default_timeout = 30
_address_length = 42
def is_valid_address(address) -> bool: def is_valid_address( address ) -> bool:
""" """
Check if given string is valid one address Check if given string is valid one address
NOTE: This function is NOT thread safe due to the C function used by the bech32 library. NOTE: This function is NOT thread safe due to the C function used by the bech32 library.
@ -40,16 +29,20 @@ def is_valid_address(address) -> bool:
bool bool
Is valid address Is valid address
""" """
if not address.startswith('one1'): if not address.startswith( "one1" ):
return False return False
hrp, _ = bech32_decode(address) hrp, _ = bech32_decode( address )
if not hrp: if not hrp:
return False return False
return True return True
def get_balance(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
""" def get_balance(
Get current account balance address,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get current account balance.
Parameters Parameters
---------- ----------
@ -74,19 +67,27 @@ def get_balance(address, endpoint=_default_endpoint, timeout=_default_timeout) -
------------- -------------
https://api.hmny.io/#da8901d2-d237-4c3b-9d7d-10af9def05c4 https://api.hmny.io/#da8901d2-d237-4c3b-9d7d-10af9def05c4
""" """
method = 'hmyv2_getBalance' method = "hmyv2_getBalance"
params = [ params = [ address ]
address
]
try: try:
balance = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] balance = rpc_request(
return int(balance) # v2 returns the result as it is method,
except TypeError as e: # check will work if rpc returns None params = params,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout
def get_balance_by_block(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int: )[ "result" ]
""" return int( balance ) # v2 returns the result as it is
Get account balance for address at a given block number except TypeError as exception: # check will work if rpc returns None
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_balance_by_block(
address,
block_num,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get account balance for address at a given block number.
Parameters Parameters
---------- ----------
@ -114,20 +115,27 @@ def get_balance_by_block(address, block_num, endpoint=_default_endpoint, timeout
https://api.hmny.io/#9aeae4b8-1a09-4ed2-956b-d7c96266dd33 https://api.hmny.io/#9aeae4b8-1a09-4ed2-956b-d7c96266dd33
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/blockchain.go#L92 https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/blockchain.go#L92
""" """
method = 'hmyv2_getBalanceByBlockNumber' method = "hmyv2_getBalanceByBlockNumber"
params = [ params = [ address, block_num ]
address,
block_num
]
try: try:
balance = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] balance = rpc_request(
return int(balance) method,
except TypeError as e: params = params,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout
def get_account_nonce(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int: )[ "result" ]
""" return int( balance )
Get the account nonce except TypeError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_account_nonce(
address,
block_num = "latest",
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get the account nonce.
Parameters Parameters
---------- ----------
@ -154,22 +162,40 @@ def get_account_nonce(address, block_num, endpoint=_default_endpoint, timeout=_d
------------- -------------
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L51 https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L51
""" """
method = 'hmyv2_getAccountNonce' method = "hmyv2_getAccountNonce"
params = [ params = [ address, block_num ]
address,
block_num
]
try: try:
nonce = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] nonce = rpc_request(
return int(nonce) method,
except TypeError as e: params = params,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout
)[ "result" ]
return int( nonce )
except TypeError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_nonce(
address,
block_num = "latest",
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""See get_account_nonce."""
return get_account_nonce( address, block_num, endpoint, timeout )
def get_transaction_count(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
""" def get_transaction_count(
Get the number of transactions the given address has sent for the given block number address,
Legacy for apiv1. For apiv2, please use get_account_nonce/get_transactions_count/get_staking_transactions_count apis for block_num,
more granular transaction counts queries endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get the number of transactions the given address has sent for the given
block number Legacy for apiv1. For apiv2, please use
get_account_nonce/get_transactions_count/get_staking_transactions_count
apis for more granular transaction counts queries.
Parameters Parameters
---------- ----------
@ -196,20 +222,27 @@ def get_transaction_count(address, block_num, endpoint=_default_endpoint, timeou
------------- -------------
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L69 https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L69
""" """
method = 'hmyv2_getTransactionCount' method = "hmyv2_getTransactionCount"
params = [ params = [ address, block_num ]
address,
block_num
]
try: try:
nonce = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] nonce = rpc_request(
return int(nonce) method,
except TypeError as e: params = params,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout
def get_transactions_count(address, tx_type, endpoint=_default_endpoint, timeout=_default_timeout) -> int: )[ "result" ]
""" return int( nonce )
Get the number of regular transactions from genesis of input type except TypeError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_transactions_count(
address,
tx_type,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get the number of regular transactions from genesis of input type.
Parameters Parameters
---------- ----------
@ -238,20 +271,28 @@ def get_transactions_count(address, tx_type, endpoint=_default_endpoint, timeout
https://api.hmny.io/#fc97aed2-e65e-4cf4-bc01-8dadb76732c0 https://api.hmny.io/#fc97aed2-e65e-4cf4-bc01-8dadb76732c0
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L114 https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L114
""" """
method = 'hmyv2_getTransactionsCount' method = "hmyv2_getTransactionsCount"
params = [ params = [ address, tx_type ]
address,
tx_type
]
try: try:
tx_count = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] tx_count = rpc_request(
return int(tx_count) method,
except TypeError as e: params = params,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout
def get_staking_transactions_count(address, tx_type, endpoint=_default_endpoint, timeout=_default_timeout) -> int: )[ "result" ]
""" return int( tx_count )
Get the number of staking transactions from genesis of input type ("SENT", "RECEIVED", "ALL") except TypeError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_staking_transactions_count(
address,
tx_type,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get the number of staking transactions from genesis of input type
("SENT", "RECEIVED", "ALL")
Parameters Parameters
---------- ----------
@ -280,22 +321,31 @@ def get_staking_transactions_count(address, tx_type, endpoint=_default_endpoint,
https://api.hmny.io/#ddc1b029-f341-4c4d-ba19-74b528d6e5e5 https://api.hmny.io/#ddc1b029-f341-4c4d-ba19-74b528d6e5e5
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L134 https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L134
""" """
method = 'hmyv2_getStakingTransactionsCount' method = "hmyv2_getStakingTransactionsCount"
params = [ params = [ address, tx_type ]
address,
tx_type
]
try: try:
tx_count = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] tx_count = rpc_request(
return int(tx_count) method,
except (KeyError, TypeError) as e: params = params,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout
def get_transaction_history(address, page=0, page_size=1000, include_full_tx=False, tx_type='ALL', )[ "result" ]
order='ASC', endpoint=_default_endpoint, timeout=_default_timeout return int( tx_count )
) -> list: except ( KeyError, TypeError ) as exception:
""" raise InvalidRPCReplyError( method, endpoint ) from exception
Get list of transactions sent and/or received by the account
def get_transaction_history( # pylint: disable=too-many-arguments
address,
page=0,
page_size=1000,
include_full_tx=False,
tx_type="ALL",
order="ASC",
endpoint=DEFAULT_ENDPOINT,
timeout=DEFAULT_TIMEOUT,
) -> list:
"""Get list of transactions sent and/or received by the account.
Parameters Parameters
---------- ----------
@ -339,26 +389,38 @@ def get_transaction_history(address, page=0, page_size=1000, include_full_tx=Fal
""" """
params = [ params = [
{ {
'address': address, "address": address,
'pageIndex': page, "pageIndex": page,
'pageSize': page_size, "pageSize": page_size,
'fullTx': include_full_tx, "fullTx": include_full_tx,
'txType': tx_type, "txType": tx_type,
'order': order "order": order,
} }
] ]
method = 'hmyv2_getTransactionsHistory' method = "hmyv2_getTransactionsHistory"
try: try:
tx_history = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout) tx_history = rpc_request(
return tx_history['result']['transactions'] method,
except KeyError as e: params = params,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout
def get_staking_transaction_history(address, page=0, page_size=1000, include_full_tx=False, tx_type='ALL', )
order='ASC', endpoint=_default_endpoint, timeout=_default_timeout return tx_history[ "result" ][ "transactions" ]
) -> list: except KeyError as exception:
""" raise InvalidRPCReplyError( method, endpoint ) from exception
Get list of staking transactions sent by the account
def get_staking_transaction_history( # pylint: disable=too-many-arguments
address,
page=0,
page_size=1000,
include_full_tx=False,
tx_type="ALL",
order="ASC",
endpoint=DEFAULT_ENDPOINT,
timeout=DEFAULT_TIMEOUT,
) -> list:
"""Get list of staking transactions sent by the account.
Parameters Parameters
---------- ----------
@ -385,7 +447,8 @@ def get_staking_transaction_history(address, page=0, page_size=1000, include_ful
------- -------
list of transactions list of transactions
if include_full_tx is True, each transaction is a dictionary with the following kets if include_full_tx is True, each transaction is a dictionary with the following kets
blockHash: :obj:`str` Block hash that transaction was finalized; "0x0000000000000000000000000000000000000000000000000000000000000000" if tx is pending blockHash: :obj:`str` Block hash that transaction was finalized or
"0x0000000000000000000000000000000000000000000000000000000000000000" if tx is pending
blockNumber: :obj:`int` Block number that transaction was finalized; None if tx is pending blockNumber: :obj:`int` Block number that transaction was finalized; None if tx is pending
from: :obj:`str` Wallet address from: :obj:`str` Wallet address
timestamp: :obj:`int` Timestamp in Unix time when transaction was finalized timestamp: :obj:`int` Timestamp in Unix time when transaction was finalized
@ -394,7 +457,8 @@ def get_staking_transaction_history(address, page=0, page_size=1000, include_ful
hash: :obj:`str` Transaction hash hash: :obj:`str` Transaction hash
nonce: :obj:`int` Wallet nonce for the transaction nonce: :obj:`int` Wallet nonce for the transaction
transactionIndex: :obj:`int` Index of transaction in block; None if tx is pending transactionIndex: :obj:`int` Index of transaction in block; None if tx is pending
type: :obj:`str` Type of staking transaction, for example, "CollectRewards", "Delegate", "Undelegate" type: :obj:`str` Type of staking transaction
for example, "CollectRewards", "Delegate", "Undelegate"
msg: :obj:`dict` Message attached to the staking transaction msg: :obj:`dict` Message attached to the staking transaction
r: :obj:`str` First 32 bytes of the transaction signature r: :obj:`str` First 32 bytes of the transaction signature
s: :obj:`str` Next 32 bytes of the transaction signature s: :obj:`str` Next 32 bytes of the transaction signature
@ -413,25 +477,36 @@ def get_staking_transaction_history(address, page=0, page_size=1000, include_ful
""" """
params = [ params = [
{ {
'address': address, "address": address,
'pageIndex': page, "pageIndex": page,
'pageSize': page_size, "pageSize": page_size,
'fullTx': include_full_tx, "fullTx": include_full_tx,
'txType': tx_type, "txType": tx_type,
'order': order "order": order,
} }
] ]
# Using v2 API, because getStakingTransactionHistory not implemented in v1 # Using v2 API, because getStakingTransactionHistory not implemented in v1
method = 'hmyv2_getStakingTransactionsHistory' method = "hmyv2_getStakingTransactionsHistory"
try: try:
stx_history = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] stx_history = rpc_request(
return stx_history['staking_transactions'] method,
except KeyError as e: params = params,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout
def get_balance_on_all_shards(address, skip_error=True, endpoint=_default_endpoint, timeout=_default_timeout) -> list: )[ "result" ]
""" return stx_history[ "staking_transactions" ]
Get current account balance in all shards & optionally report errors getting account balance for a shard except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_balance_on_all_shards(
address,
skip_error = True,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get current account balance in all shards & optionally report errors
getting account balance for a shard.
Parameters Parameters
---------- ----------
@ -458,24 +533,39 @@ def get_balance_on_all_shards(address, skip_error=True, endpoint=_default_endpoi
] ]
""" """
balances = [] balances = []
sharding_structure = get_sharding_structure(endpoint=endpoint, timeout=timeout) sharding_structure = get_sharding_structure(
endpoint = endpoint,
timeout = timeout
)
for shard in sharding_structure: for shard in sharding_structure:
try: try:
balances.append({ balances.append(
'shard': shard['shardID'], {
'balance': get_balance(address, endpoint=shard['http'], timeout=timeout) "shard": shard[ "shardID" ],
}) "balance": get_balance(
except (KeyError, RPCError, RequestsError, RequestsTimeoutError): address,
endpoint = shard[ "http" ],
timeout = timeout
),
}
)
except ( KeyError, RPCError, RequestsError, RequestsTimeoutError ):
if not skip_error: if not skip_error:
balances.append({ balances.append(
'shard': shard['shardID'], {
'balance': None "shard": shard[ "shardID" ],
}) "balance": None
}
)
return balances return balances
def get_total_balance(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
""" def get_total_balance(
Get total account balance on all shards address,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get total account balance on all shards.
Parameters Parameters
---------- ----------
@ -501,7 +591,12 @@ def get_total_balance(address, endpoint=_default_endpoint, timeout=_default_time
get_balance_on_all_shards get_balance_on_all_shards
""" """
try: try:
balances = get_balance_on_all_shards(address, skip_error=False, endpoint=endpoint, timeout=timeout) balances = get_balance_on_all_shards(
return sum(b['balance'] for b in balances) address,
except TypeError as e: skip_error = False,
raise RuntimeError from e endpoint = endpoint,
timeout = timeout
)
return sum( b[ "balance" ] for b in balances )
except TypeError as exception:
raise RuntimeError from exception

@ -17,107 +17,106 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE. # THE SOFTWARE.
"""Reference implementation for Bech32 and segwit addresses.""" """Reference implementation for Bech32 and segwit addresses."""
CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" CHARSET = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
def bech32_polymod(values): def bech32_polymod( values ):
"""Internal function that computes the Bech32 checksum.""" """Internal function that computes the Bech32 checksum."""
generator = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3] generator = [ 0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3 ]
chk = 1 chk = 1
for value in values: for value in values:
top = chk >> 25 top = chk >> 25
chk = (chk & 0x1ffffff) << 5 ^ value chk = ( chk & 0x1FFFFFF ) << 5 ^ value
for i in range(5): for i in range( 5 ):
chk ^= generator[i] if ((top >> i) & 1) else 0 chk ^= generator[ i ] if ( ( top >> i ) & 1 ) else 0
return chk return chk
def bech32_hrp_expand(hrp): def bech32_hrp_expand( hrp ):
"""Expand the HRP into values for checksum computation.""" """Expand the HRP into values for checksum computation."""
return [ord(x) >> 5 for x in hrp] + [0] + [ord(x) & 31 for x in hrp] return [ ord( x ) >> 5 for x in hrp ] + [ 0
] + [ ord( x ) & 31 for x in hrp ]
def bech32_verify_checksum(hrp, data): def bech32_verify_checksum( hrp, data ):
"""Verify a checksum given HRP and converted data characters.""" """Verify a checksum given HRP and converted data characters."""
return bech32_polymod(bech32_hrp_expand(hrp) + data) == 1 return bech32_polymod( bech32_hrp_expand( hrp ) + data ) == 1
def bech32_create_checksum(hrp, data): def bech32_create_checksum( hrp, data ):
"""Compute the checksum values given HRP and data.""" """Compute the checksum values given HRP and data."""
values = bech32_hrp_expand(hrp) + data values = bech32_hrp_expand( hrp ) + data
polymod = bech32_polymod(values + [0, 0, 0, 0, 0, 0]) ^ 1 polymod = bech32_polymod( values + [ 0, 0, 0, 0, 0, 0 ] ) ^ 1
return [(polymod >> 5 * (5 - i)) & 31 for i in range(6)] return [ ( polymod >> 5 * ( 5 - i ) ) & 31 for i in range( 6 ) ]
def bech32_encode(hrp, data): def bech32_encode( hrp, data ):
"""Compute a Bech32 string given HRP and data values.""" """Compute a Bech32 string given HRP and data values."""
combined = data + bech32_create_checksum(hrp, data) combined = data + bech32_create_checksum( hrp, data )
return hrp + '1' + ''.join([CHARSET[d] for d in combined]) return hrp + "1" + "".join( [ CHARSET[ d ] for d in combined ] )
def bech32_decode(bech): def bech32_decode( bech ):
"""Validate a Bech32 string, and determine HRP and data.""" """Validate a Bech32 string, and determine HRP and data."""
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or if ( any( ord( x ) < 33 or ord( x ) > 126 for x in bech
(bech.lower() != bech and bech.upper() != bech)): ) ) or ( bech.lower() != bech and bech.upper() != bech ):
return (None, None) return ( None, None )
bech = bech.lower() bech = bech.lower()
pos = bech.rfind('1') pos = bech.rfind( "1" )
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90: if pos < 1 or pos + 7 > len( bech ) or len( bech ) > 90:
return (None, None) return ( None, None )
if not all(x in CHARSET for x in bech[pos+1:]): if not all( x in CHARSET for x in bech[ pos + 1 : ] ):
return (None, None) return ( None, None )
hrp = bech[:pos] hrp = bech[ : pos ]
data = [CHARSET.find(x) for x in bech[pos+1:]] data = [ CHARSET.find( x ) for x in bech[ pos + 1 : ] ]
if not bech32_verify_checksum(hrp, data): if not bech32_verify_checksum( hrp, data ):
return (None, None) return ( None, None )
return (hrp, data[:-6]) return ( hrp, data[ :-6 ] )
def convertbits(data, frombits, tobits, pad=True): def convertbits( data, frombits, tobits, pad = True ):
"""General power-of-2 base conversion.""" """General power-of-2 base conversion."""
acc = 0 acc = 0
bits = 0 bits = 0
ret = [] ret = []
maxv = (1 << tobits) - 1 maxv = ( 1 << tobits ) - 1
max_acc = (1 << (frombits + tobits - 1)) - 1 max_acc = ( 1 << ( frombits + tobits - 1 ) ) - 1
for value in data: for value in data:
if value < 0 or (value >> frombits): if value < 0 or ( value >> frombits ):
return None return None
acc = ((acc << frombits) | value) & max_acc acc = ( ( acc << frombits ) | value ) & max_acc
bits += frombits bits += frombits
while bits >= tobits: while bits >= tobits:
bits -= tobits bits -= tobits
ret.append((acc >> bits) & maxv) ret.append( ( acc >> bits ) & maxv )
if pad: if pad:
if bits: if bits:
ret.append((acc << (tobits - bits)) & maxv) ret.append( ( acc << ( tobits - bits ) ) & maxv )
elif bits >= frombits or ((acc << (tobits - bits)) & maxv): elif bits >= frombits or ( ( acc << ( tobits - bits ) ) & maxv ):
return None return None
return ret return ret
def decode(hrp, addr): def decode( hrp, addr ):
"""Decode a segwit address.""" """Decode a segwit address."""
hrpgot, data = bech32_decode(addr) hrpgot, data = bech32_decode( addr )
if hrpgot != hrp: if hrpgot != hrp:
return (None, None) return ( None, None )
decoded = convertbits(data[1:], 5, 8, False) decoded = convertbits( data[ 1 : ], 5, 8, False )
if decoded is None or len(decoded) < 2 or len(decoded) > 40: if decoded is None or len( decoded ) < 2 or len( decoded ) > 40:
return (None, None) return ( None, None )
if data[0] > 16: if data[ 0 ] > 16:
return (None, None) return ( None, None )
if data[0] == 0 and len(decoded) != 20 and len(decoded) != 32: if data[ 0 ] == 0 and len( decoded ) != 20 and len( decoded ) != 32:
return (None, None) return ( None, None )
return (data[0], decoded) return ( data[ 0 ], decoded )
def encode(hrp, witver, witprog): def encode( hrp, witver, witprog ):
"""Encode a segwit address.""" """Encode a segwit address."""
ret = bech32_encode(hrp, [witver] + convertbits(witprog, 8, 5)) ret = bech32_encode( hrp, [ witver ] + convertbits( witprog, 8, 5 ) )
if decode(hrp, ret) == (None, None): if decode( hrp, ret ) == ( None, None ):
return None return None
return ret return ret

File diff suppressed because it is too large Load Diff

@ -7,7 +7,8 @@ Example:
Below is a demo of how to import, manage keys, and interact with the CLI:: Below is a demo of how to import, manage keys, and interact with the CLI::
>>> from pyhmy import cli >>> from pyhmy import cli
>>> cli.single_call("hmy keys add test1") >>> cli.single_call("hmy keys add test1")
'**Important** write this seed phrase in a safe place, it is the only way to recover your account if you ever forget your password '**Important** write this seed phrase in a safe place,
it is the only way to recover your account if you ever forget your password
craft ... tobacco' craft ... tobacco'
>>> cli.get_accounts_keystore() >>> cli.get_accounts_keystore()
{'test1': 'one1aqfeed538xf7n0cfh60tjaeat7yw333pmj6sfu'} {'test1': 'one1aqfeed538xf7n0cfh60tjaeat7yw333pmj6sfu'}
@ -40,128 +41,167 @@ For more details, reference the documentation here: TODO gitbook docs
""" """
import subprocess import subprocess
import pexpect
import os import os
import shutil import shutil
import re import re
import stat import stat
import sys import sys
from multiprocessing import Lock
from pathlib import Path from pathlib import Path
import pexpect
import requests import requests
from .util import get_bls_build_variables, get_gopath from .util import get_bls_build_variables, get_gopath
if sys.platform.startswith("linux"): if sys.platform.startswith( "linux" ):
_libs = {"libbls384_256.so", "libcrypto.so.10", "libgmp.so.10", "libgmpxx.so.4", "libmcl.so"} _libs = {
"libbls384_256.so",
"libcrypto.so.10",
"libgmp.so.10",
"libgmpxx.so.4",
"libmcl.so",
}
else: else:
_libs = {"libbls384_256.dylib", "libcrypto.1.0.0.dylib", "libgmp.10.dylib", "libgmpxx.4.dylib", "libmcl.dylib"} _libs = {
_accounts = {} # Internal accounts keystore, make sure to sync when needed. "libbls384_256.dylib",
_account_keystore_path = "~/.hmy/account-keys" # Internal path to account keystore, will match the current binary. "libcrypto.1.0.0.dylib",
_binary_path = "hmy" # Internal binary path. "libgmp.10.dylib",
_arg_prefix = "__PYHMY_ARG_PREFIX__" "libgmpxx.4.dylib",
_keystore_cache_lock = Lock() "libmcl.dylib",
}
# Internal accounts keystore, make sure to sync when needed.
_accounts = {}
# Internal path to account keystore, will match the current binary.
ARG_PREFIX = "__PYHMY_ARG_PREFIX__"
# _keystore_cache_lock = Lock()
environment = os.environ.copy() # The environment for the CLI to execute in. environment = os.environ.copy() # The environment for the CLI to execute in.
# completely remove caching...
# we need to improve getting address better internally to REDUCE single calls....
# def _cache_and_lock_accounts_keystore(fn):
# """Internal decorator to cache the accounts keystore and prevent concurrent
# accesses with locks."""
# cached_accounts = {}
# last_mod = None
# def wrap(*args):
# nonlocal last_mod
# _keystore_cache_lock.acquire()
# files_in_dir = str(os.listdir(ACCOUNT_KEYSTORE_PATH))
# dir_mod_time = str(os.path.getmtime(ACCOUNT_KEYSTORE_PATH))
# curr_mod = hash(files_in_dir + dir_mod_time + BINARY_PATH)
# if curr_mod != last_mod:
# cached_accounts.clear()
# cached_accounts.update(fn(*args))
# last_mod = curr_mod
# accounts = cached_accounts.copy()
# _keystore_cache_lock.release()
# return accounts
# return wrap
# TODO: completely remove caching... we need to improve getting address better internally to REDUCE single calls....
def _cache_and_lock_accounts_keystore(fn): def account_keystore_path( value = None ):
""" """
Internal decorator to cache the accounts keystore and Gets or sets the ACCOUNT_KEYSTORE_PATH
prevent concurrent accesses with locks.
""" """
cached_accounts = {} if "value" not in account_keystore_path.__dict__:
last_mod = None account_keystore_path.value = "~/.hmy/account-keys"
if value:
account_keystore_path.value = value
return account_keystore_path.value
def wrap(*args):
nonlocal last_mod
_keystore_cache_lock.acquire()
files_in_dir = str(os.listdir(_account_keystore_path))
dir_mod_time = str(os.path.getmtime(_account_keystore_path))
curr_mod = hash(files_in_dir + dir_mod_time + _binary_path)
if curr_mod != last_mod:
cached_accounts.clear()
cached_accounts.update(fn(*args))
last_mod = curr_mod
accounts = cached_accounts.copy()
_keystore_cache_lock.release()
return accounts
return wrap def binary_path( value = None ):
"""
Gets or sets the BINARY_PATH
"""
if "value" not in binary_path.__dict__:
binary_path.value = "hmy"
if value:
binary_path.value = value
return binary_path.value
def _get_current_accounts_keystore(): def _get_current_accounts_keystore():
""" """Internal function that gets the current keystore from the CLI.
Internal function that gets the current keystore from the CLI.
:returns A dictionary where the keys are the account names/aliases and the :returns A dictionary where the keys are the account names/aliases and the
values are their 'one1...' addresses. values are their 'one1...' addresses.
""" """
curr_addresses = {} curr_addresses = {}
response = single_call("hmy keys list") response = single_call( "hmy keys list" )
lines = response.split("\n") lines = response.split( "\n" )
if "NAME" not in lines[0] or "ADDRESS" not in lines[0]: if "NAME" not in lines[ 0 ] or "ADDRESS" not in lines[ 0 ]:
raise ValueError("Name or Address not found on first line of key list") raise ValueError(
if lines[1] != "": "Name or Address not found on first line of key list"
raise ValueError("Unknown format: No blank line between label and data") )
for line in lines[2:]: if lines[ 1 ] != "":
columns = line.split("\t") raise ValueError(
if len(columns) != 2: "Unknown format: No blank line between label and data"
)
for line in lines[ 2 : ]:
columns = line.split( "\t" )
if len( columns ) != 2:
break # Done iterating through all of the addresses. break # Done iterating through all of the addresses.
name, address = columns name, address = columns
curr_addresses[name.strip()] = address curr_addresses[ name.strip() ] = address
return curr_addresses return curr_addresses
def _set_account_keystore_path(): def _set_account_keystore_path():
""" """Internal function to set the account keystore path according to the
Internal function to set the account keystore path according to the binary. binary."""
""" response = single_call( "hmy keys location" ).strip()
global _account_keystore_path if not os.path.exists( response ):
response = single_call("hmy keys location").strip() os.mkdir( response )
if not os.path.exists(response): account_keystore_path( response )
os.mkdir(response)
_account_keystore_path = response
def _sync_accounts(): def _sync_accounts():
""" """Internal function that UPDATES the accounts keystore with the CLI's
Internal function that UPDATES the accounts keystore with the CLI's keystore. keystore."""
"""
new_keystore = _get_current_accounts_keystore() new_keystore = _get_current_accounts_keystore()
for key in new_keystore.keys(): for key, value in new_keystore.items():
if key not in _accounts.keys(): if key not in _accounts:
_accounts[key] = new_keystore[key] _accounts[ key ] = value
acc_keys_to_remove = [k for k in _accounts.keys() if k not in new_keystore.keys()] acc_keys_to_remove = [ k for k in _accounts if k not in new_keystore ]
for key in acc_keys_to_remove: for key in acc_keys_to_remove:
del _accounts[key] del _accounts[ key ]
def _make_call_command(command): def _make_call_command( command ):
""" """Internal function that processes a command String or String Arg List for
Internal function that processes a command String or String Arg List for
underlying pexpect or subprocess call. underlying pexpect or subprocess call.
Note that single quote is not respected for strings. Note that single quote is not respected for strings.
""" """
if isinstance(command, list): if isinstance( command, list ):
command_toks = command command_toks = command
else: else:
all_strings = sorted(re.findall(r'"(.*?)"', command), key=lambda e: len(e), reverse=True) all_strings = sorted(
for i, string in enumerate(all_strings): re.findall(r'"(.*?)"', command),
command = command.replace(string, f"{_arg_prefix}_{i}") key=lambda e: len(e), # pylint: disable=unnecessary-lambda
command_toks_prefix = [el for el in command.split(" ") if el] reverse=True
)
for i, string in enumerate( all_strings ):
command = command.replace( string, f"{ARG_PREFIX}_{i}" )
command_toks_prefix = [ el for el in command.split( " " ) if el ]
command_toks = [] command_toks = []
for el in command_toks_prefix: for element in command_toks_prefix:
if el.startswith(f'"{_arg_prefix}_') and el.endswith(f'"'): if element.startswith( f'"{ARG_PREFIX}_'
index = int(el.replace(f'"{_arg_prefix}_', '').replace('"', '')) ) and element.endswith( '"' ):
command_toks.append(all_strings[index]) index = int(
element.replace( f'"{ARG_PREFIX}_',
"" ).replace( '"',
"" )
)
command_toks.append( all_strings[ index ] )
else: else:
command_toks.append(el) command_toks.append( element )
if re.match(".*hmy", command_toks[0]): if re.match( ".*hmy", command_toks[ 0 ] ):
command_toks = command_toks[1:] command_toks = command_toks[ 1 : ]
return command_toks return command_toks
@ -175,38 +215,46 @@ def get_accounts_keystore():
return _accounts return _accounts
def is_valid_binary(path): def is_valid_binary( path ):
""" """
:param path: Path to the Harmony CLI binary (absolute or relative). :param path: Path to the Harmony CLI binary (absolute or relative).
:return: If the file at the path is a CLI binary. :return: If the file at the path is a CLI binary.
""" """
path = os.path.realpath(path) path = os.path.realpath( path )
os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC) os.chmod( path, os.stat( path ).st_mode | stat.S_IEXEC )
try: try:
proc = subprocess.Popen([path, "version"], env=environment, with subprocess.Popen(
stdout=subprocess.PIPE, stderr=subprocess.PIPE) [ path,
out, err = proc.communicate() "version" ],
env = environment,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
) as proc:
_, err = proc.communicate()
if not err: if not err:
return False return False
return "harmony" in err.decode().strip().lower() return "harmony" in err.decode().strip().lower()
except (OSError, subprocess.CalledProcessError, subprocess.SubprocessError): except (
OSError,
subprocess.CalledProcessError,
subprocess.SubprocessError
):
return False return False
def set_binary(path): def set_binary( path ):
""" """
:param path: The path of the CLI binary to use. :param path: The path of the CLI binary to use.
:returns If the binary has been set. :returns If the binary has been set.
Note that the exposed keystore will be updated accordingly. Note that the exposed keystore will be updated accordingly.
""" """
global _binary_path path = os.path.realpath( path )
path = os.path.realpath(path) assert os.path.exists( path )
assert os.path.exists(path) os.chmod( path, os.stat( path ).st_mode | stat.S_IEXEC )
os.chmod(path, os.stat(path).st_mode | stat.S_IEXEC) if not is_valid_binary( path ):
if not is_valid_binary(path):
return False return False
_binary_path = path binary_path( path )
_set_account_keystore_path() _set_account_keystore_path()
_sync_accounts() _sync_accounts()
return True return True
@ -216,19 +264,26 @@ def get_binary_path():
""" """
:return: The absolute path of the CLI binary. :return: The absolute path of the CLI binary.
""" """
return os.path.abspath(_binary_path) return os.path.abspath( binary_path() )
def get_version(): def get_version():
""" """
:return: The version string of the CLI binary. :return: The version string of the CLI binary.
""" """
proc = subprocess.Popen([_binary_path, "version"], env=environment, with subprocess.Popen(
stdout=subprocess.PIPE, stderr=subprocess.PIPE) [ binary_path(),
out, err = proc.communicate() "version" ],
env = environment,
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
) as proc:
_, err = proc.communicate()
if not err: if not err:
raise RuntimeError(f"Could not get version.\n" raise RuntimeError(
f"\tGot exit code {proc.returncode}. Expected non-empty error message.") f"Could not get version.\n"
f"\tGot exit code {proc.returncode}. Expected non-empty error message."
)
return err.decode().strip() return err.decode().strip()
@ -236,10 +291,10 @@ def get_account_keystore_path():
""" """
:return: The absolute path to the account keystore of the CLI binary. :return: The absolute path to the account keystore of the CLI binary.
""" """
return os.path.abspath(_account_keystore_path) return os.path.abspath( account_keystore_path() )
def check_address(address): def check_address( address ):
""" """
:param address: A 'one1...' address. :param address: A 'one1...' address.
:return: Boolean of if the address is in the CLI's keystore. :return: Boolean of if the address is in the CLI's keystore.
@ -247,15 +302,15 @@ def check_address(address):
return address in get_accounts_keystore().values() return address in get_accounts_keystore().values()
def get_address(name): def get_address( name ):
""" """
:param name: The alias of a key used in the CLI's keystore. :param name: The alias of a key used in the CLI's keystore.
:return: The associated 'one1...' address. :return: The associated 'one1...' address.
""" """
return get_accounts_keystore().get(name, None) return get_accounts_keystore().get( name, None )
def get_accounts(address): def get_accounts( address ):
""" """
:param address: The 'one1...' address :param address: The 'one1...' address
:return: A list of account names associated with the param :return: A list of account names associated with the param
@ -263,38 +318,42 @@ def get_accounts(address):
Note that a list of account names is needed because 1 address can Note that a list of account names is needed because 1 address can
have multiple names within the CLI's keystore. have multiple names within the CLI's keystore.
""" """
return [acc for acc, addr in get_accounts_keystore().items() if address == addr] return [
acc for acc,
addr in get_accounts_keystore().items() if address == addr
]
def remove_account(name): def remove_account( name ):
""" """Note that this edits the keystore directly since there is currently no
Note that this edits the keystore directly since there is currently no
way to remove an address using the CLI. way to remove an address using the CLI.
:param name: The alias of a key used in the CLI's keystore. :param name: The alias of a key used in the CLI's keystore.
:raises RuntimeError: If it failed to remove an account. :raises RuntimeError: If it failed to remove an account.
""" """
if not get_address(name): if not get_address( name ):
return return
keystore_path = f"{get_account_keystore_path()}/{name}" keystore_path = f"{get_account_keystore_path()}/{name}"
try: try:
shutil.rmtree(keystore_path) shutil.rmtree( keystore_path )
except (shutil.Error, FileNotFoundError) as err: except ( shutil.Error, FileNotFoundError ) as err:
raise RuntimeError(f"Failed to delete dir: {keystore_path}\n" raise RuntimeError(
f"\tException: {err}") from err f"Failed to delete dir: {keystore_path}\n"
f"\tException: {err}"
) from err
_sync_accounts() _sync_accounts()
def remove_address(address): def remove_address( address ):
""" """
:param address: The 'one1...' address to be removed. :param address: The 'one1...' address to be removed.
""" """
for name in get_accounts(address): for name in get_accounts( address ):
remove_account(name) remove_account( name )
_sync_accounts() _sync_accounts()
def single_call(command, timeout=60, error_ok=False): def single_call( command, timeout = 60, error_ok = False ):
""" """
:param command: String or String Arg List of command to execute on CLI. :param command: String or String Arg List of command to execute on CLI.
:param timeout: Optional timeout in seconds :param timeout: Optional timeout in seconds
@ -302,82 +361,129 @@ def single_call(command, timeout=60, error_ok=False):
:returns: Decoded string of response from hmy CLI call :returns: Decoded string of response from hmy CLI call
:raises: RuntimeError if bad command :raises: RuntimeError if bad command
""" """
command_toks = [_binary_path] + _make_call_command(command) command_toks = [ binary_path() ] + _make_call_command( command )
try: try:
return subprocess.check_output(command_toks, env=environment, timeout=timeout).decode() return subprocess.check_output(
command_toks,
env = environment,
timeout = timeout
).decode()
except subprocess.CalledProcessError as err: except subprocess.CalledProcessError as err:
if not error_ok: if not error_ok:
raise RuntimeError(f"Bad CLI args: `{command}`\n " raise RuntimeError(
f"\tException: {err}") from err f"Bad CLI args: `{command}`\n "
f"\tException: {err}"
) from err
return err.output.decode() return err.output.decode()
def expect_call(command, timeout=60): def expect_call( command, timeout = 60 ):
""" """
:param command: String or String Arg List of command to execute on CLI. :param command: String or String Arg List of command to execute on CLI.
:param timeout: Optional timeout in seconds :param timeout: Optional timeout in seconds
:returns: A pexpect child program :returns: A pexpect child program
:raises: RuntimeError if bad command :raises: RuntimeError if bad command
""" """
command_toks = _make_call_command(command) command_toks = _make_call_command( command )
try: try:
proc = pexpect.spawn(f"{_binary_path}", command_toks, env=environment, timeout=timeout) proc = pexpect.spawn(
f"{binary_path()}",
command_toks,
env = environment,
timeout = timeout
)
proc.delaybeforesend = None proc.delaybeforesend = None
except pexpect.ExceptionPexpect as err: except pexpect.ExceptionPexpect as err:
raise RuntimeError(f"Bad CLI args: `{command}`\n " raise RuntimeError(
f"\tException: {err}") from err f"Bad CLI args: `{command}`\n "
f"\tException: {err}"
) from err
return proc return proc
def download(path="./bin/hmy", replace=True, verbose=True): def download( path = "./bin/hmy", replace = True, verbose = True ):
""" """Download the CLI binary to the specified path. Related files will be
Download the CLI binary to the specified path. saved in the same directory.
Related files will be saved in the same directory.
:param path: The desired path (absolute or relative) of the saved binary. :param path: The desired path (absolute or relative) of the saved binary.
:param replace: A flag to force a replacement of the binary/file. :param replace: A flag to force a replacement of the binary/file.
:param verbose: A flag to enable a report message once the binary is downloaded. :param verbose: A flag to enable a report message once the binary is downloaded.
:returns the environment to run the saved CLI binary. :returns the environment to run the saved CLI binary.
""" """
path = os.path.realpath(path) path = os.path.realpath( path )
parent_dir = Path(path).parent parent_dir = Path( path ).parent
assert not os.path.isdir(path), f"path `{path}` must specify a file, not a directory." assert not os.path.isdir(
path
), f"path `{path}` must specify a file, not a directory."
if not os.path.exists(path) or replace: if not os.path.exists( path ) or replace:
old_cwd = os.getcwd() old_cwd = os.getcwd()
os.makedirs(parent_dir, exist_ok=True) os.makedirs( parent_dir, exist_ok = True )
os.chdir(parent_dir) os.chdir( parent_dir )
hmy_script_path = os.path.join(parent_dir, "hmy.sh") hmy_script_path = os.path.join( parent_dir, "hmy.sh" )
with open(hmy_script_path, 'w') as f: with open( hmy_script_path, "w", encoding = 'utf8' ) as script_file:
f.write(requests.get("https://raw.githubusercontent.com/harmony-one/go-sdk/master/scripts/hmy.sh") script_file.write(
.content.decode()) requests.get(
os.chmod(hmy_script_path, os.stat(hmy_script_path).st_mode | stat.S_IEXEC) "https://raw.githubusercontent.com/harmony-one/go-sdk/master/scripts/hmy.sh"
).content.decode()
)
os.chmod(
hmy_script_path,
os.stat( hmy_script_path ).st_mode | stat.S_IEXEC
)
same_name_file = False same_name_file = False
if os.path.exists(os.path.join(parent_dir, "hmy")) and Path(path).name != "hmy": # Save same name file. if (
os.path.exists( os.path.join( parent_dir,
"hmy" ) ) and
Path( path ).name != "hmy"
): # Save same name file.
same_name_file = True same_name_file = True
os.rename(os.path.join(parent_dir, "hmy"), os.path.join(parent_dir, ".hmy_tmp")) os.rename(
os.path.join( parent_dir,
"hmy" ),
os.path.join( parent_dir,
".hmy_tmp" )
)
if verbose: if verbose:
subprocess.call([hmy_script_path, '-d']) subprocess.call( [ hmy_script_path, "-d" ] )
else: else:
subprocess.call([hmy_script_path, '-d'], stdout=open(os.devnull, 'w'), stderr=subprocess.STDOUT) with open( os.devnull, "w", encoding = "UTF-8" ) as devnull:
os.rename(os.path.join(parent_dir, "hmy"), path) subprocess.call(
[ hmy_script_path,
"-d" ],
stdout = devnull,
stderr = subprocess.STDOUT,
)
os.rename( os.path.join( parent_dir, "hmy" ), path )
if same_name_file: if same_name_file:
os.rename(os.path.join(parent_dir, ".hmy_tmp"), os.path.join(parent_dir, "hmy")) os.rename(
os.path.join( parent_dir,
".hmy_tmp" ),
os.path.join( parent_dir,
"hmy" )
)
if verbose: if verbose:
print(f"Saved harmony binary to: `{path}`") print( f"Saved harmony binary to: `{path}`" )
os.chdir(old_cwd) os.chdir( old_cwd )
env = os.environ.copy() env = os.environ.copy()
if sys.platform.startswith("darwin"): # Dynamic linking for darwin if sys.platform.startswith( "darwin" ): # Dynamic linking for darwin
try: try:
files_in_parent_dir = set(os.listdir(parent_dir)) files_in_parent_dir = set( os.listdir( parent_dir ) )
if files_in_parent_dir.intersection(_libs) == _libs: if files_in_parent_dir.intersection( _libs ) == _libs:
env["DYLD_FALLBACK_LIBRARY_PATH"] = parent_dir env[ "DYLD_FALLBACK_LIBRARY_PATH" ] = parent_dir
elif os.path.exists(f"{get_gopath()}/src/github.com/harmony-one/bls") \ elif os.path.exists(
and os.path.exists(f"{get_gopath()}/src/github.com/harmony-one/mcl"): f"{get_gopath()}/src/github.com/harmony-one/bls"
env.update(get_bls_build_variables()) ) and os.path.exists(
f"{get_gopath()}/src/github.com/harmony-one/mcl"
):
env.update( get_bls_build_variables() )
else: else:
raise RuntimeWarning(f"Could not get environment for downloaded hmy CLI at `{path}`") raise RuntimeWarning(
except Exception as e: f"Could not get environment for downloaded hmy CLI at `{path}`"
raise RuntimeWarning(f"Could not get environment for downloaded hmy CLI at `{path}`") from e )
except Exception as exception:
raise RuntimeWarning(
f"Could not get environment for downloaded hmy CLI at `{path}`"
) from exception
return env return env

@ -0,0 +1,18 @@
"""
Constants
"""
# localnet constants
DEFAULT_ENDPOINT = 'http://localhost:9500'
DEFAULT_TIMEOUT = 30
# staking percentage constants
PRECISION = 18
MAX_DECIMAL = 1000000000000000000
NAME_CHAR_LIMIT = 140
IDENTITY_CHAR_LIMIT = 140
WEBSITE_CHAR_LIMIT = 140
SECURITY_CONTACT_CHAR_LIMIT = 140
DETAILS_CHAR_LIMIT = 280
MIN_REQUIRED_DELEGATION = int( 10000 * 1e18 )

@ -1,27 +1,36 @@
from .rpc.request import ( """
rpc_request Basic smart contract functions on Harmony
) For full ABI driven interaction, use something like web3py or brownie
"""
from .transaction import ( from .rpc.request import rpc_request
get_transaction_receipt
)
from .transaction import get_transaction_receipt
_default_endpoint = 'http://localhost:9500' from .exceptions import InvalidRPCReplyError
_default_timeout = 30
from .constants import DEFAULT_ENDPOINT, DEFAULT_TIMEOUT
######################### #########################
# Smart contract RPCs # Smart contract RPCs
######################### #########################
def call(to, block_num, from_address=None, gas=None, gas_price=None, value=None, data=None, def call( # pylint: disable=too-many-arguments
endpoint=_default_endpoint, timeout=_default_timeout) -> str: to_address,
""" block_num,
Execute a smart contract without saving state from_address=None,
gas=None,
gas_price=None,
value=None,
data=None,
endpoint=DEFAULT_ENDPOINT,
timeout=DEFAULT_TIMEOUT,
) -> str:
"""Execute a smart contract without saving state.
Parameters Parameters
---------- ----------
to: :obj:`str` to_address: :obj:`str`
Address of the smart contract Address of the smart contract
block_num: :obj:`int` block_num: :obj:`int`
Block number to execute the contract for Block number to execute the contract for
@ -48,7 +57,7 @@ def call(to, block_num, from_address=None, gas=None, gas_price=None, value=None,
Raises Raises
------ ------
InvalidRPCReplyError InvalidRPCReplyError
If received unknown result from endpoint, or If received unknown result from exceptionndpoint, or
API Reference API Reference
------------- -------------
@ -56,29 +65,42 @@ def call(to, block_num, from_address=None, gas=None, gas_price=None, value=None,
""" """
params = [ params = [
{ {
'to': to, "to": to_address,
'from': from_address, "from": from_address,
'gas': gas, "gas": gas,
'gasPrice': gas_price, "gasPrice": gas_price,
'value': value, "value": value,
'data': data "data": data,
}, },
block_num block_num,
] ]
method = 'hmyv2_call' method = "hmyv2_call"
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def estimate_gas(to, from_address=None, gas=None, gas_price=None, value=None, data=None,
endpoint=_default_endpoint, timeout=_default_timeout) -> int: def estimate_gas( # pylint: disable=too-many-arguments
""" to_address,
Estimate the gas price needed for a smart contract call from_address=None,
gas=None,
gas_price=None,
value=None,
data=None,
endpoint=DEFAULT_ENDPOINT,
timeout=DEFAULT_TIMEOUT,
) -> int:
"""Estimate the gas price needed for a smart contract call.
Parameters Parameters
---------- ----------
to: :obj:`str` to_address: :obj:`str`
Address of the smart contract Address of the smart contract
from_address: :obj:`str`, optional from_address: :obj:`str`, optional
Wallet address Wallet address
@ -103,29 +125,45 @@ def estimate_gas(to, from_address=None, gas=None, gas_price=None, value=None, da
Raises Raises
------ ------
InvalidRPCReplyError InvalidRPCReplyError
If received unknown result from endpoint, or If received unknown result from exceptionndpoint, or
API Reference API Reference
------------- -------------
https://api.hmny.io/?version=latest#b9bbfe71-8127-4dda-b26c-ff95c4c22abd https://api.hmny.io/?version=latest#b9bbfe71-8127-4dda-b26c-ff95c4c22abd
""" """
params = [ { params = [
'to': to, {
'from': from_address, "to": to_address,
'gas': gas, "from": from_address,
'gasPrice': gas_price, "gas": gas,
'value': value, "gasPrice": gas_price,
'data': data "value": value,
} ] "data": data,
method = 'hmyv2_estimateGas' }
]
method = "hmyv2_estimateGas"
try: try:
return int(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'], 16) return int(
except KeyError as e: rpc_request(
raise InvalidRPCReplyError(method, endpoint) from e method,
params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ],
16,
)
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_code(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> str:
""" def get_code(
Get the code stored at the given address in the state for the given block number address,
block_num,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> str:
"""Get the code stored at the given address in the state for the given
block number.
Parameters Parameters
---------- ----------
@ -146,26 +184,35 @@ def get_code(address, block_num, endpoint=_default_endpoint, timeout=_default_ti
Raises Raises
------ ------
InvalidRPCReplyError InvalidRPCReplyError
If received unknown result from endpoint, or If received unknown result from exceptionndpoint, or
API Reference API Reference
------------- -------------
https://api.hmny.io/?version=latest#e13e9d78-9322-4dc8-8917-f2e721a8e556 https://api.hmny.io/?version=latest#e13e9d78-9322-4dc8-8917-f2e721a8e556
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/contract.go#L59 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/contract.go#L59
""" """
params = [ params = [ address, block_num ]
address, method = "hmyv2_getCode"
block_num
]
method = 'hmyv2_getCode'
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_storage_at(address, key, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> str:
""" def get_storage_at(
Get the storage from the state at the given address, the key and the block number address,
key,
block_num,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> str:
"""Get the storage from the state at the given address, the key and the
block number.
Parameters Parameters
---------- ----------
@ -188,28 +235,33 @@ def get_storage_at(address, key, block_num, endpoint=_default_endpoint, timeout=
Raises Raises
------ ------
InvalidRPCReplyError InvalidRPCReplyError
If received unknown result from endpoint, or If received unknown result from exceptionndpoint, or
API Reference API Reference
------------- -------------
https://api.hmny.io/?version=latest#fa8ac8bd-952d-4149-968c-857ca76da43f https://api.hmny.io/?version=latest#fa8ac8bd-952d-4149-968c-857ca76da43f
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/contract.go#L84 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/contract.go#L84
""" """
params = [ params = [ address, key, block_num ]
address, method = "hmyv2_getStorageAt"
key,
block_num
]
method = 'hmyv2_getStorageAt'
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_contract_address_from_hash(tx_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> str:
""" def get_contract_address_from_hash(
Get address of the contract which was deployed in the transaction tx_hash,
represented by tx_hash endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> str:
"""Get address of the contract which was deployed in the transaction
represented by tx_hash.
Parameters Parameters
---------- ----------
@ -228,13 +280,18 @@ def get_contract_address_from_hash(tx_hash, endpoint=_default_endpoint, timeout=
Raises Raises
------ ------
InvalidRPCReplyError InvalidRPCReplyError
If received unknown result from endpoint, or If received unknown result from exceptionndpoint, or
API Reference API Reference
------------- -------------
https://github.com/harmony-one/harmony-test/blob/master/localnet/rpc_tests/test_contract.py#L36 https://github.com/harmony-one/harmony-test/blob/master/localnet/rpc_tests/test_contract.py#L36
""" """
try: try:
return get_transaction_receipt(tx_hash, endpoint, timeout)["contractAddress"] return get_transaction_receipt( tx_hash,
except KeyError as e: endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout )[ "contractAddress" ]
except KeyError as exception:
raise InvalidRPCReplyError(
"hmyv2_getTransactionReceipt",
endpoint
) from exception

@ -1,44 +1,38 @@
from .rpc.exceptions import ( """
RPCError, Exceptions used by pyhmy
RequestsError, """
RequestsTimeoutError
)
class InvalidRPCReplyError( RuntimeError ):
class InvalidRPCReplyError(RuntimeError): """Exception raised when RPC call returns unexpected result Generally
""" indicates Harmony API has been updated & pyhmy library needs to be updated
Exception raised when RPC call returns unexpected result as well."""
Generally indicates Harmony API has been updated & pyhmy library needs to be updated as well def __init__( self, method, endpoint ):
""" super().__init__( f"Unexpected reply for {method} from {endpoint}" )
def __init__(self, method, endpoint):
super().__init__(f'Unexpected reply for {method} from {endpoint}') class InvalidValidatorError( ValueError ):
"""Exception raised Validator does not pass sanity checks."""
class InvalidValidatorError(ValueError):
"""
Exception raised Validator does not pass sanity checks
"""
errors = { errors = {
1: 'Invalid ONE address', 1: "Invalid ONE address",
2: 'Field not initialized', 2: "Field not initialized",
3: 'Invalid field input', 3: "Invalid field input",
4: 'Error checking blockchain', 4: "Error checking blockchain",
5: 'Unable to import validator information from blockchain' 5: "Unable to import validator information from blockchain",
} }
def __init__(self, err_code, msg): def __init__( self, err_code, msg ):
self.code = err_code self.code = err_code
self.msg = msg self.msg = msg
super().__init__(msg) super().__init__( msg )
def __str__(self): def __str__( self ):
return f'[Errno {self.code}] {self.errors[self.code]}: {self.msg}' return f"[Errno {self.code}] {self.errors[self.code]}: {self.msg}"
class TxConfirmationTimedoutError(AssertionError):
"""
Exception raised when a transaction is sent to the chain
But not confirmed during the timeout period specified
"""
def __init__(self, msg): class TxConfirmationTimedoutError( AssertionError ):
super().__init__(f'{msg}') """Exception raised when a transaction is sent to the chain But not
confirmed during the timeout period specified."""
def __init__( self, msg ):
super().__init__( f"{msg}" )

@ -1,3 +1,7 @@
"""
Logger for pyhmy
"""
import threading import threading
import datetime import datetime
import gzip import gzip
@ -6,39 +10,41 @@ import logging
import logging.handlers import logging.handlers
class _GZipRotator: class _GZipRotator: # pylint: disable=too-few-public-methods
def __call__(self, source, dest): def __call__( self, source, dest ):
os.rename(source, dest) os.rename( source, dest )
f_in = open(dest, 'rb') with open( dest, "rb" ) as f_in:
f_out = gzip.open("%s.gz" % dest, 'wb') with gzip.open( f"{dest}.gz", "wb" ) as f_out:
f_out.writelines(f_in) f_out.writelines( f_in )
f_out.close() os.remove( dest )
f_in.close()
os.remove(dest)
class ControlledLogger:
"""
A simple logger that only writes to file when the 'write' method is called.
"""
def __init__(self, logger_name, log_dir, backup_count=5): class ControlledLogger: # pylint: disable=too-many-instance-attributes
"""A simple logger that only writes to file when the 'write' method is
called."""
def __init__( self, logger_name, log_dir, backup_count = 5 ):
""" """
:param logger_name: The name of the logger and logfile :param logger_name: The name of the logger and logfile
:param log_dir: The directory in which to save this log file (can be abs or relative). :param log_dir: The directory in which to save this log file (can be abs or relative).
""" """
if log_dir.endswith('/'): if log_dir.endswith( "/" ):
log_dir = log_dir[:-1] log_dir = log_dir[ :-1 ]
log_dir = os.path.realpath(log_dir) log_dir = os.path.realpath( log_dir )
os.makedirs(log_dir, exist_ok=True) os.makedirs( log_dir, exist_ok = True )
handler = logging.handlers.TimedRotatingFileHandler(f"{log_dir}/{logger_name}.log", 'midnight', 1, handler = logging.handlers.TimedRotatingFileHandler(
backupCount=backup_count) f"{log_dir}/{logger_name}.log",
handler.setFormatter(logging.Formatter('%(levelname)s - %(message)s')) "midnight",
1,
backupCount = backup_count
)
handler.setFormatter(
logging.Formatter( "%(levelname)s - %(message)s" )
)
handler.rotator = _GZipRotator() handler.rotator = _GZipRotator()
self.filename = handler.baseFilename self.filename = handler.baseFilename
self.logger = logging.getLogger(logger_name) self.logger = logging.getLogger( logger_name )
self.logger.addHandler(handler) self.logger.addHandler( handler )
self._lock = threading.Lock() self._lock = threading.Lock()
self.filepath = f"{log_dir}/{logger_name}.log" self.filepath = f"{log_dir}/{logger_name}.log"
self.info_buffer = [] self.info_buffer = []
@ -46,97 +52,92 @@ class ControlledLogger:
self.warning_buffer = [] self.warning_buffer = []
self.error_buffer = [] self.error_buffer = []
def __repr__(self): def __repr__( self ):
return f"<ControlledLogger @ {self.filepath} : {self.logger}>" return f"<ControlledLogger @ {self.filepath} : {self.logger}>"
def _clear(self): def _clear( self ):
""" """Internal method to clear the log buffer."""
Internal method to clear the log buffer.
"""
self.info_buffer.clear() self.info_buffer.clear()
self.debug_buffer.clear() self.debug_buffer.clear()
self.warning_buffer.clear() self.warning_buffer.clear()
self.error_buffer.clear() self.error_buffer.clear()
def info(self, msg): def info( self, msg ):
""" """
:param msg: The info message to log :param msg: The info message to log
""" """
self._lock.acquire() with self._lock:
self.info_buffer.append(f"[{threading.get_ident()}] " self.info_buffer.append(
f"{datetime.datetime.utcnow()} : {msg}") f"[{threading.get_ident()}] "
self._lock.release() f"{datetime.datetime.utcnow()} : {msg}"
)
def debug(self, msg): def debug( self, msg ):
""" """
:param msg: The debug message to log :param msg: The debug message to log
""" """
self._lock.acquire() with self._lock:
self.debug_buffer.append(f"[{threading.get_ident()}] " self.debug_buffer.append(
f"{datetime.datetime.utcnow()} : {msg}") f"[{threading.get_ident()}] "
self._lock.release() f"{datetime.datetime.utcnow()} : {msg}"
)
def warning(self, msg): def warning( self, msg ):
""" """
:param msg: The warning message to log :param msg: The warning message to log
""" """
self._lock.acquire() with self._lock:
self.warning_buffer.append(f"[{threading.get_ident()}] " self.warning_buffer.append(
f"{datetime.datetime.utcnow()} : {msg}") f"[{threading.get_ident()}] "
self._lock.release() f"{datetime.datetime.utcnow()} : {msg}"
)
def error(self, msg): def error( self, msg ):
""" """
:param msg: The error message to log :param msg: The error message to log
""" """
self._lock.acquire() with self._lock:
self.error_buffer.append(f"[{threading.get_ident()}] " self.error_buffer.append(
f"{datetime.datetime.utcnow()} : {msg}") f"[{threading.get_ident()}] "
self._lock.release() f"{datetime.datetime.utcnow()} : {msg}"
)
def print_info(self): def print_info( self ):
""" """Prints the current info buffer but does not flush it to log file."""
Prints the current info buffer but does not flush it to log file. print( "\n".join( self.info_buffer ) )
"""
print('\n'.join(self.info_buffer))
def print_debug(self): def print_debug( self ):
""" """Prints the current debug buffer but does not flush it to log
Prints the current debug buffer but does not flush it to log file. file."""
""" print( "\n".join( self.debug_buffer ) )
print('\n'.join(self.debug_buffer))
def print_warning(self): def print_warning( self ):
""" """Prints the current warning buffer but does not flush it to log
Prints the current warning buffer but does not flush it to log file. file."""
""" print( "\n".join( self.warning_buffer ) )
print('\n'.join(self.warning_buffer))
def print_error(self): def print_error( self ):
""" """Prints the current error buffer but does not flush it to log
Prints the current error buffer but does not flush it to log file. file."""
""" print( "\n".join( self.error_buffer ) )
print('\n'.join(self.error_buffer))
def write(self): def write( self ):
""" """Flushes ALL of the log buffers to the log file via the logger.
Flushes ALL of the log buffers to the log file via the logger.
Note that directly after this method call, the respective prints will print Note that directly after this method call, the respective prints
nothing since all log messages are flushed to file. will print nothing since all log messages are flushed to file.
""" """
self._lock.acquire() with self._lock:
self.logger.setLevel(logging.DEBUG) self.logger.setLevel( logging.DEBUG )
for line in self.debug_buffer: for line in self.debug_buffer:
self.logger.debug(line) self.logger.debug( line )
self.logger.setLevel(logging.WARNING) self.logger.setLevel( logging.WARNING )
for line in self.warning_buffer: for line in self.warning_buffer:
self.logger.warning(line) self.logger.warning( line )
self.logger.setLevel(logging.ERROR) self.logger.setLevel( logging.ERROR )
for line in self.error_buffer: for line in self.error_buffer:
self.logger.error(line) self.logger.error( line )
self.logger.setLevel(logging.INFO) self.logger.setLevel( logging.INFO )
for line in self.info_buffer: for line in self.info_buffer:
self.logger.info(line) self.logger.info( line )
self._clear() self._clear()
self._lock.release()

@ -1,12 +1,15 @@
from decimal import Decimal """
Handles conversion of ONE to ATTO and vice versa
For more granular conversions, see Web3.toWei
"""
from decimal import Decimal
_conversion_unit = Decimal(1e18) _conversion_unit = Decimal( 1e18 )
def convert_atto_to_one(atto) -> Decimal: def convert_atto_to_one( atto ) -> Decimal:
""" """Convert ATTO to ONE.
Convert ATTO to ONE
Parameters Parameters
---------- ----------
@ -19,14 +22,13 @@ def convert_atto_to_one(atto) -> Decimal:
decimal decimal
Converted value in ONE Converted value in ONE
""" """
if isinstance(atto, float): if isinstance( atto, float ):
atto = int(atto) atto = int( atto )
return Decimal(atto) / _conversion_unit return Decimal( atto ) / _conversion_unit
def convert_one_to_atto(one) -> Decimal: def convert_one_to_atto( one ) -> Decimal:
""" """Convert ONE to ATTO.
Convert ONE to ATTO
Parameters Parameters
---------- ----------
@ -38,6 +40,6 @@ def convert_one_to_atto(one) -> Decimal:
decimal decimal
Converted value in ATTO Converted value in ATTO
""" """
if isinstance(one, float): if isinstance( one, float ):
one = str(one) one = str( one )
return Decimal(one) * _conversion_unit return Decimal( one ) * _conversion_unit

@ -1,27 +1,26 @@
import requests """
RPC Specific Exceptions
"""
import requests
class RPCError(RuntimeError):
"""
Exception raised when RPC call returns an error
"""
def __init__(self, method, endpoint, error): class RPCError( RuntimeError ):
"""Exception raised when RPC call returns an error."""
def __init__( self, method, endpoint, error ):
self.error = error self.error = error
super().__init__(f'Error in reply from {endpoint}: {method} returned {error}') super().__init__(
f"Error in reply from {endpoint}: {method} returned {error}"
)
class RequestsError(requests.exceptions.RequestException):
"""
Wrapper for requests lib exceptions
"""
def __init__(self, endpoint): class RequestsError( requests.exceptions.RequestException ):
super().__init__(f'Error connecting to {endpoint}') """Wrapper for requests lib exceptions."""
def __init__( self, endpoint ):
super().__init__( f"Error connecting to {endpoint}" )
class RequestsTimeoutError(requests.exceptions.Timeout):
"""
Wrapper for requests lib Timeout exceptions
"""
def __init__(self, endpoint): class RequestsTimeoutError( requests.exceptions.Timeout ):
super().__init__(f'Error connecting to {endpoint}') """Wrapper for requests lib Timeout exceptions."""
def __init__( self, endpoint ):
super().__init__( f"Error connecting to {endpoint}" )

@ -1,21 +1,22 @@
"""
RPC wrapper around requests library
"""
import json import json
import requests import requests
from .exceptions import ( from .exceptions import RequestsError, RequestsTimeoutError, RPCError
RequestsError,
RequestsTimeoutError,
RPCError
)
_default_endpoint = 'http://localhost:9500' from ..constants import DEFAULT_ENDPOINT, DEFAULT_TIMEOUT
_default_timeout = 30
def base_request(method, params=None, endpoint=_default_endpoint, timeout=_default_timeout) -> str: def base_request(
""" method,
Basic RPC request params = None,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> str:
"""Basic RPC request.
Parameters Parameters
--------- ---------
@ -44,8 +45,8 @@ def base_request(method, params=None, endpoint=_default_endpoint, timeout=_defau
""" """
if params is None: if params is None:
params = [] params = []
elif not isinstance(params, list): elif not isinstance( params, list ):
raise TypeError(f'invalid type {params.__class__}') raise TypeError( f"invalid type {params.__class__}" )
try: try:
payload = { payload = {
@ -55,21 +56,31 @@ def base_request(method, params=None, endpoint=_default_endpoint, timeout=_defau
"params": params "params": params
} }
headers = { headers = {
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
resp = requests.request('POST', endpoint, headers=headers, data=json.dumps(payload), resp = requests.request(
timeout=timeout, allow_redirects=True) "POST",
endpoint,
headers = headers,
data = json.dumps( payload ),
timeout = timeout,
allow_redirects = True,
)
return resp.content return resp.content
except requests.exceptions.Timeout as err: except requests.exceptions.Timeout as err:
raise RequestsTimeoutError(endpoint) from err raise RequestsTimeoutError( endpoint ) from err
except requests.exceptions.RequestException as err: except requests.exceptions.RequestException as err:
raise RequestsError(endpoint) from err raise RequestsError( endpoint ) from err
def rpc_request(method, params=None, endpoint=_default_endpoint, timeout=_default_timeout) -> dict: def rpc_request(
""" method,
RPC request params = None,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""RPC request.
Parameters Parameters
--------- ---------
@ -102,15 +113,12 @@ def rpc_request(method, params=None, endpoint=_default_endpoint, timeout=_defaul
-------- --------
base_request base_request
""" """
raw_resp = base_request(method, params, endpoint, timeout) raw_resp = base_request( method, params, endpoint, timeout )
try: try:
resp = json.loads(raw_resp) resp = json.loads( raw_resp )
if 'error' in resp: if "error" in resp:
raise RPCError(method, endpoint, str(resp['error'])) raise RPCError( method, endpoint, str( resp[ "error" ] ) )
return resp return resp
except json.decoder.JSONDecodeError as err: except json.decoder.JSONDecodeError as err:
raise RPCError(method, endpoint, raw_resp) from err raise RPCError( method, endpoint, raw_resp ) from err
# TODO: Add GET requests

@ -1,133 +1,163 @@
import rlp """
Sign Harmony or Ethereum transactions
Harmony staking transaction signing is not covered by this module
"""
from eth_utils.curried import ( # pylint: disable=protected-access, no-member
keccak,
to_int,
hexstr_if_str,
apply_formatters_to_dict
)
from rlp.sedes import ( from functools import partial
big_endian_int, from toolz import dissoc, pipe, merge
Binary,
binary
)
from eth_account import ( import rlp
Account
)
from eth_rlp import ( from eth_utils.curried import keccak, to_int, hexstr_if_str, apply_formatters_to_dict
HashableRLP
)
from hexbytes import ( from rlp.sedes import big_endian_int, Binary, binary
HexBytes
)
from eth_account._utils.signing import ( from eth_rlp import HashableRLP
sign_transaction_hash
)
from eth_account._utils.transactions import ( from hexbytes import HexBytes
from eth_account import Account
from eth_account.datastructures import SignedTransaction
from eth_account._utils.legacy_transactions import (
Transaction as SignedEthereumTxData, Transaction as SignedEthereumTxData,
UnsignedTransaction as UnsignedEthereumTxData, UnsignedTransaction as UnsignedEthereumTxData,
TRANSACTION_FORMATTERS as ETHEREUM_FORMATTERS, LEGACY_TRANSACTION_FORMATTERS as ETHEREUM_FORMATTERS,
TRANSACTION_DEFAULTS, TRANSACTION_DEFAULTS,
chain_id_to_v, chain_id_to_v,
UNSIGNED_TRANSACTION_FIELDS
)
from cytoolz import (
dissoc,
pipe,
merge,
partial
) )
from eth_account._utils.signing import sign_transaction_hash
from eth_account.datastructures import ( from .util import chain_id_to_int, convert_one_to_hex
SignedTransaction
)
from .util import (
chain_id_to_int,
convert_one_to_hex
)
HARMONY_FORMATTERS = dict( HARMONY_FORMATTERS = dict(
ETHEREUM_FORMATTERS, ETHEREUM_FORMATTERS,
shardID=hexstr_if_str(to_int), # additional fields for Harmony transaction shardID=hexstr_if_str(to_int), # additional fields for Harmony transaction
toShardID=hexstr_if_str(to_int), # which may be cross shard toShardID=hexstr_if_str(to_int), # which may be cross shard
) )
class UnsignedHarmonyTxData(HashableRLP): class UnsignedHarmonyTxData( HashableRLP ):
"""
Unsigned Harmony transaction data
Includes `shardID` and `toShardID`
as the difference against Eth
"""
fields = ( fields = (
('nonce', big_endian_int), ( "nonce",
('gasPrice', big_endian_int), big_endian_int ),
('gas', big_endian_int), ( "gasPrice",
('shardID', big_endian_int), big_endian_int ),
('toShardID', big_endian_int), ( "gas",
('to', Binary.fixed_length(20, allow_empty=True)), big_endian_int ),
('value', big_endian_int), ( "shardID",
('data', binary), big_endian_int ),
( "toShardID",
big_endian_int ),
( "to",
Binary.fixed_length( 20,
allow_empty = True ) ),
( "value",
big_endian_int ),
( "data",
binary ),
) )
class SignedHarmonyTxData(HashableRLP):
class SignedHarmonyTxData( HashableRLP ):
"""
Signed Harmony transaction data
Includes `shardID` and `toShardID`
as the difference against Eth
"""
fields = UnsignedHarmonyTxData._meta.fields + ( fields = UnsignedHarmonyTxData._meta.fields + (
("v", big_endian_int), # Recovery value + 27 ("v", big_endian_int), # Recovery value + 27
("r", big_endian_int), # First 32 bytes ("r", big_endian_int), # First 32 bytes
("s", big_endian_int), # Next 32 bytes ("s", big_endian_int), # Next 32 bytes
) )
def encode_transaction(unsigned_transaction, vrs): # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L55
'''serialize and encode an unsigned transaction with v,r,s''' # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L55
(v, r, s) = vrs def encode_transaction( unsigned_transaction, vrs ):
"""serialize and encode an unsigned transaction with v,r,s."""
( v, r, s ) = vrs # pylint: disable=invalid-name
chain_naive_transaction = dissoc( chain_naive_transaction = dissoc(
unsigned_transaction.as_dict(), 'v', 'r', 's') unsigned_transaction.as_dict(),
if isinstance(unsigned_transaction, (UnsignedHarmonyTxData, "v",
SignedHarmonyTxData)): "r",
"s"
)
if isinstance(
unsigned_transaction,
( UnsignedHarmonyTxData,
SignedHarmonyTxData )
):
serializer = SignedHarmonyTxData serializer = SignedHarmonyTxData
else: else:
serializer = SignedEthereumTxData serializer = SignedEthereumTxData
signed_transaction = serializer(v=v, r=r, s=s, **chain_naive_transaction) signed_transaction = serializer(
return rlp.encode(signed_transaction) v = v,
r = r,
s = s,
**chain_naive_transaction
)
return rlp.encode( signed_transaction )
def serialize_transaction(filled_transaction):
'''serialize a signed/unsigned transaction''' def serialize_transaction( filled_transaction ):
if 'v' in filled_transaction: """serialize a signed/unsigned transaction."""
if 'shardID' in filled_transaction: if "v" in filled_transaction:
if "shardID" in filled_transaction:
serializer = SignedHarmonyTxData serializer = SignedHarmonyTxData
else: else:
serializer = SignedEthereumTxData serializer = SignedEthereumTxData
else: else:
if 'shardID' in filled_transaction: if "shardID" in filled_transaction:
serializer = UnsignedHarmonyTxData serializer = UnsignedHarmonyTxData
else: else:
serializer = UnsignedEthereumTxData serializer = UnsignedEthereumTxData
for f, _ in serializer._meta.fields: for field, _ in serializer._meta.fields:
assert f in filled_transaction, f'Could not find {f} in transaction' assert field in filled_transaction, f"Could not find {field} in transaction"
return serializer.from_dict({f: filled_transaction[f] for f, _ in serializer._meta.fields}) return serializer.from_dict(
{
def sanitize_transaction(transaction_dict, private_key): field: filled_transaction[ field ]
'''remove the originating address from the dict and convert chainId to int''' for field,
account = Account.from_key(private_key) # get account, from which you can derive public + private key _ in serializer._meta.fields
transaction_dict = transaction_dict.copy() # do not alter the original dictionary }
if 'from' in transaction_dict: )
transaction_dict[ 'from' ] = convert_one_to_hex( transaction_dict[ 'from' ] )
if transaction_dict[ 'from' ] == account.address: # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/account.py#L650
sanitized_transaction = dissoc(transaction_dict, 'from') # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/account.py#L650
def sanitize_transaction( transaction_dict, private_key ):
"""remove the originating address from the dict and convert chainId to
int."""
account = Account.from_key( # pylint: disable=no-value-for-parameter
private_key
)
sanitized_transaction = transaction_dict.copy(
) # do not alter the original dictionary
if "from" in sanitized_transaction:
sanitized_transaction[ "from" ] = convert_one_to_hex(
transaction_dict[ "from" ]
)
if sanitized_transaction[ "from" ] == account.address:
sanitized_transaction = dissoc( sanitized_transaction, "from" )
else: else:
raise TypeError("from field must match key's %s, but it was %s" % ( raise TypeError(
account.address, "from field must match key's {account.address}, "
transaction_dict['from'], "but it was {sanitized_transaction['from']}"
)) )
if 'chainId' in transaction_dict: if "chainId" in sanitized_transaction:
transaction_dict[ 'chainId' ] = chain_id_to_int( transaction_dict[ 'chainId' ] ) sanitized_transaction[ "chainId" ] = chain_id_to_int(
return account, transaction_dict sanitized_transaction[ "chainId" ]
)
def sign_transaction(transaction_dict, private_key) -> SignedTransaction: return account, sanitized_transaction
"""
Sign a (non-staking) transaction dictionary with the specified private key
def sign_transaction( transaction_dict, private_key ) -> SignedTransaction:
"""Sign a (non-staking) transaction dictionary with the specified private
key.
Parameters Parameters
---------- ----------
@ -171,30 +201,50 @@ def sign_transaction(transaction_dict, private_key) -> SignedTransaction:
https://readthedocs.org/projects/eth-account/downloads/pdf/stable/ https://readthedocs.org/projects/eth-account/downloads/pdf/stable/
""" """
account, sanitized_transaction = sanitize_transaction(transaction_dict, private_key) account, sanitized_transaction = sanitize_transaction(transaction_dict, private_key)
if 'to' in sanitized_transaction and sanitized_transaction[ 'to' ] is not None: if "to" in sanitized_transaction and sanitized_transaction[ "to"
sanitized_transaction[ 'to' ] = convert_one_to_hex( sanitized_transaction[ 'to' ] ) ] is not None:
filled_transaction = pipe( # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L39 sanitized_transaction[ "to" ] = convert_one_to_hex(
sanitized_transaction[ "to" ]
)
# https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L39
filled_transaction = pipe(
sanitized_transaction, sanitized_transaction,
dict, dict,
partial(merge, TRANSACTION_DEFAULTS), partial( merge,
TRANSACTION_DEFAULTS ),
chain_id_to_v, chain_id_to_v,
apply_formatters_to_dict(HARMONY_FORMATTERS) apply_formatters_to_dict( HARMONY_FORMATTERS ),
) )
unsigned_transaction = serialize_transaction(filled_transaction) unsigned_transaction = serialize_transaction( filled_transaction )
transaction_hash = unsigned_transaction.hash() transaction_hash = unsigned_transaction.hash()
if isinstance(unsigned_transaction, (UnsignedEthereumTxData, UnsignedHarmonyTxData)): # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/signing.py#L26
chain_id = None # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/signing.py#L26 if isinstance(
unsigned_transaction,
( UnsignedEthereumTxData,
UnsignedHarmonyTxData )
):
chain_id = None
else: else:
chain_id = unsigned_transaction.v chain_id = unsigned_transaction.v
(v, r, s) = sign_transaction_hash( ( v, # pylint: disable=invalid-name
account._key_obj, transaction_hash, chain_id) r, # pylint: disable=invalid-name
encoded_transaction = encode_transaction(unsigned_transaction, vrs=(v, r, s)) s ) = sign_transaction_hash( # pylint: disable=invalid-name
signed_transaction_hash = keccak(encoded_transaction) account._key_obj,
transaction_hash,
chain_id
)
encoded_transaction = encode_transaction(
unsigned_transaction,
vrs = ( v,
r,
s )
)
signed_transaction_hash = keccak( encoded_transaction )
return SignedTransaction( return SignedTransaction(
rawTransaction=HexBytes(encoded_transaction), rawTransaction = HexBytes( encoded_transaction ),
hash=HexBytes(signed_transaction_hash), hash = HexBytes( signed_transaction_hash ),
r=r, r = r,
s=s, s = s,
v=v, v = v,
) )

@ -1,16 +1,22 @@
from .rpc.request import ( """
rpc_request Call Harmony's staking API
) """
from .rpc.request import rpc_request
from .exceptions import InvalidRPCReplyError
from .constants import DEFAULT_ENDPOINT, DEFAULT_TIMEOUT
_default_endpoint = 'http://localhost:9500'
_default_timeout = 30
################## ##################
# Validator RPCs # # Validator RPCs #
################## ##################
def get_all_validator_addresses(endpoint=_default_endpoint, timeout=_default_timeout) -> list: def get_all_validator_addresses(
""" endpoint = DEFAULT_ENDPOINT,
Get list of all created validator addresses on chain timeout = DEFAULT_TIMEOUT
) -> list:
"""Get list of all created validator addresses on chain.
Parameters Parameters
---------- ----------
@ -33,15 +39,21 @@ def get_all_validator_addresses(endpoint=_default_endpoint, timeout=_default_tim
------------- -------------
https://api.hmny.io/#69b93657-8d3c-4d20-9c9f-e51f08c9b3f5 https://api.hmny.io/#69b93657-8d3c-4d20-9c9f-e51f08c9b3f5
""" """
method = 'hmyv2_getAllValidatorAddresses' method = "hmyv2_getAllValidatorAddresses"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_validator_information(validator_addr, endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
""" def get_validator_information(
Get validator information for validator address validator_addr,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get validator information for validator address.
Parameters Parameters
---------- ----------
@ -60,7 +72,8 @@ def get_validator_information(validator_addr, endpoint=_default_endpoint, timeou
bls-public-keys: :obj:`list` List of associated public BLS keys bls-public-keys: :obj:`list` List of associated public BLS keys
last-epoch-in-committee: :obj:`int` Last epoch any key of the validator was elected last-epoch-in-committee: :obj:`int` Last epoch any key of the validator was elected
min-self-delegation: :obj:`int` Amount that validator must delegate to self in ATTO min-self-delegation: :obj:`int` Amount that validator must delegate to self in ATTO
max-total-delegation: :obj:`int` Total amount that validator will aceept delegations until, in ATTO max-total-delegation: :obj:`int`
Total amount that validator will aceept delegations until, in ATTO
rate: :obj:`str` Current commission rate rate: :obj:`str` Current commission rate
max-rate: :obj:`str` Max commission rate a validator can charge max-rate: :obj:`str` Max commission rate a validator can charge
max-change-rate: :obj:`str` Maximum amount the commission rate can increase in one epoch max-change-rate: :obj:`str` Maximum amount the commission rate can increase in one epoch
@ -71,8 +84,9 @@ def get_validator_information(validator_addr, endpoint=_default_endpoint, timeou
security-contact: :obj:`str` Method to contact the validators security-contact: :obj:`str` Method to contact the validators
details: :obj:`str` Validator details, displayed on the Staking Dashboard details: :obj:`str` Validator details, displayed on the Staking Dashboard
creation-height: :obj:`int` Block number in which the validator was created creation-height: :obj:`int` Block number in which the validator was created
delegations: :obj:`list` List of delegations, see get_delegations_by_delegator for format delegations: :obj:`list`
metrics: :obj:`dict` BLS key earning metrics for current epoch (or None if no earnings in the current epoch) List of delegations, see get_delegations_by_delegator for format
metrics: :obj:`dict` BLS key earning metrics for current epoch (or None)
by-bls-key: :obj:`list` List of dictionaries, each with the following keys by-bls-key: :obj:`list` List of dictionaries, each with the following keys
key: :obj:`dict` Dictionary with the following keys key: :obj:`dict` Dictionary with the following keys
bls-public-key: :obj:`str` BLS public key bls-public-key: :obj:`str` BLS public key
@ -85,7 +99,8 @@ def get_validator_information(validator_addr, endpoint=_default_endpoint, timeou
earned-reward: :obj:`int` Lifetime reward key has earned earned-reward: :obj:`int` Lifetime reward key has earned
total-delegation: :obj:`int` Total amount delegated to validator total-delegation: :obj:`int` Total amount delegated to validator
currently-in-committee: :obj:`bool` if key is currently elected currently-in-committee: :obj:`bool` if key is currently elected
epos-status: :obj:`str` Currently elected, eligible to be elected next epoch, or not eligible to be elected next epoch epos-status: :obj:`str` Currently elected, eligible to be elected next epoch,
or not eligible to be elected next epoch
epos-winning-stake: :obj:`str` Total effective stake of the validator epos-winning-stake: :obj:`str` Total effective stake of the validator
booted-status: :obj:`str` Banned status booted-status: :obj:`str` Banned status
active-status: :obj:`str` Active or inactive active-status: :obj:`str` Active or inactive
@ -108,18 +123,24 @@ def get_validator_information(validator_addr, endpoint=_default_endpoint, timeou
------------- -------------
https://api.hmny.io/#659ad999-14ca-4498-8f74-08ed347cab49 https://api.hmny.io/#659ad999-14ca-4498-8f74-08ed347cab49
""" """
method = 'hmyv2_getValidatorInformation' method = "hmyv2_getValidatorInformation"
params = [ params = [ validator_addr ]
validator_addr
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def get_elected_validator_addresses(endpoint=_default_endpoint, timeout=_default_timeout) -> list: timeout = timeout
""" )[ "result" ]
Get list of elected validator addresses except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_elected_validator_addresses(
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get list of elected validator addresses.
Parameters Parameters
---------- ----------
@ -143,15 +164,21 @@ def get_elected_validator_addresses(endpoint=_default_endpoint, timeout=_default
------------- -------------
https://api.hmny.io/#e90a6131-d67c-4110-96ef-b283d452632d https://api.hmny.io/#e90a6131-d67c-4110-96ef-b283d452632d
""" """
method = 'hmyv2_getElectedValidatorAddresses' method = "hmyv2_getElectedValidatorAddresses"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_validators(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
""" def get_validators(
Get validators list for a particular epoch epoch,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get validators list for a particular epoch.
Parameters Parameters
---------- ----------
@ -179,18 +206,25 @@ def get_validators(epoch, endpoint=_default_endpoint, timeout=_default_timeout)
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L152 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L152
""" """
method = 'hmyv2_getValidators' method = "hmyv2_getValidators"
params = [ params = [ epoch ]
epoch
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def get_validator_keys(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> list: timeout = timeout
""" )[ "result" ]
Get validator BLS keys in the committee for a particular epoch except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_validator_keys(
epoch,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get validator BLS keys in the committee for a particular epoch.
Parameters Parameters
---------- ----------
@ -214,18 +248,26 @@ def get_validator_keys(epoch, endpoint=_default_endpoint, timeout=_default_timeo
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L152 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L152
""" """
method = 'hmyv2_getValidatorKeys' method = "hmyv2_getValidatorKeys"
params = [ params = [ epoch ]
epoch
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_validator_information_by_block_number(validator_addr, block_num, endpoint=_default_endpoint, timeout=_default_timeout):
""" def get_validator_information_by_block_number(
Get validator information for validator address at a block validator_addr,
block_num,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
):
"""Get validator information for validator address at a block.
Parameters Parameters
---------- ----------
@ -251,24 +293,30 @@ def get_validator_information_by_block_number(validator_addr, block_num, endpoin
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L319 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L319
""" """
method = 'hmyv2_getValidatorInformationByBlockNumber' method = "hmyv2_getValidatorInformationByBlockNumber"
params = [ params = [ validator_addr, block_num ]
validator_addr,
block_num
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def get_all_validator_information(page=-1, endpoint=_default_endpoint, timeout=_default_timeout) -> list: timeout = timeout
""" )[ "result" ]
Get validator information for all validators on chain except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_all_validator_information(
page = 0,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get validator information for all validators on chain.
Parameters Parameters
---------- ----------
page: :obj:`int`, optional page: :obj:`int`, optional
Page to request (-1 for all validators), page size is 100 otherwise Page to request, page size is 100 otherwise
endpoint: :obj:`str`, optional endpoint: :obj:`str`, optional
Endpoint to send request to Endpoint to send request to
timeout: :obj:`int`, optional timeout: :obj:`int`, optional
@ -287,18 +335,25 @@ def get_all_validator_information(page=-1, endpoint=_default_endpoint, timeout=_
------------- -------------
https://api.hmny.io/#df5f1631-7397-48e8-87b4-8dd873235b9c https://api.hmny.io/#df5f1631-7397-48e8-87b4-8dd873235b9c
""" """
method = 'hmyv2_getAllValidatorInformation' method = "hmyv2_getAllValidatorInformation"
params = [ params = [ page ]
page
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def get_validator_self_delegation(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int: timeout = timeout
""" )[ "result" ]
Get the amount self delegated by validator except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_validator_self_delegation(
address,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get the amount self delegated by validator.
Parameters Parameters
---------- ----------
@ -322,18 +377,27 @@ def get_validator_self_delegation(address, endpoint=_default_endpoint, timeout=_
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L352 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L352
""" """
method = 'hmyv2_getValidatorSelfDelegation' method = "hmyv2_getValidatorSelfDelegation"
params = [ params = [ address ]
address
]
try: try:
return int(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']) return int(
except (KeyError, TypeError) as e: rpc_request(
raise InvalidRPCReplyError(method, endpoint) from e method,
params = params,
def get_validator_total_delegation(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int: endpoint = endpoint,
""" timeout = timeout
Get the total amount delegated t ovalidator (including self delegated) )[ "result" ]
)
except ( KeyError, TypeError ) as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_validator_total_delegation(
address,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get the total amount delegated t ovalidator (including self delegated)
Parameters Parameters
---------- ----------
@ -357,25 +421,35 @@ def get_validator_total_delegation(address, endpoint=_default_endpoint, timeout=
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L379 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L379
""" """
method = 'hmyv2_getValidatorTotalDelegation' method = "hmyv2_getValidatorTotalDelegation"
params = [ params = [ address ]
address
]
try: try:
return int(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']) return int(
except (KeyError, TypeError) as e: rpc_request(
raise InvalidRPCReplyError(method, endpoint) from e method,
params = params,
def get_all_validator_information_by_block_number(block_num, page=-1, endpoint=_default_endpoint, timeout=_default_timeout) -> list: endpoint = endpoint,
""" timeout = timeout
Get validator information at block number for all validators on chain )[ "result" ]
)
except ( KeyError, TypeError ) as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_all_validator_information_by_block_number(
block_num,
page = 0,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get validator information at block number for all validators on chain.
Parameters Parameters
---------- ----------
block_num: int block_num: int
Block number to get validator information for Block number to get validator information for
page: :obj:`int`, optional page: :obj:`int`, optional
Page to request (-1 for all validators), page size is 100 Page to request, page size is 100
endpoint: :obj:`str`, optional endpoint: :obj:`str`, optional
Endpoint to send request to Endpoint to send request to
timeout: :obj:`int`, optional timeout: :obj:`int`, optional
@ -395,27 +469,33 @@ def get_all_validator_information_by_block_number(block_num, page=-1, endpoint=_
------------- -------------
https://api.hmny.io/#a229253f-ca76-4b9d-88f5-9fd96e40d583 https://api.hmny.io/#a229253f-ca76-4b9d-88f5-9fd96e40d583
""" """
method = 'hmyv2_getAllValidatorInformationByBlockNumber' method = "hmyv2_getAllValidatorInformationByBlockNumber"
params = [ params = [ page, block_num ]
page,
block_num
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
################### ###################
# Delegation RPCs # # Delegation RPCs #
################### ###################
def get_all_delegation_information(page=-1, endpoint=_default_endpoint, timeout=_default_timeout) -> list: def get_all_delegation_information(
""" page = 0,
Get delegation information for all delegators on chain endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get delegation information for all delegators on chain.
Parameters Parameters
---------- ----------
page: :obj:`int`, optional page: :obj:`int`, optional
Page to request (-1 for all validators), page size is 100 Page to request, page size is 100
endpoint: :obj:`str`, optional endpoint: :obj:`str`, optional
Endpoint to send request to Endpoint to send request to
timeout: :obj:`int`, optional timeout: :obj:`int`, optional
@ -436,18 +516,25 @@ def get_all_delegation_information(page=-1, endpoint=_default_endpoint, timeout=
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L413 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L413
""" """
method = 'hmyv2_getAllDelegationInformation' method = "hmyv2_getAllDelegationInformation"
params = [ params = [ page, ]
page,
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_delegations_by_delegator(delegator_addr, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
""" def get_delegations_by_delegator(
Get list of delegations by a delegator delegator_addr,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get list of delegations by a delegator.
Parameters Parameters
---------- ----------
@ -478,18 +565,26 @@ def get_delegations_by_delegator(delegator_addr, endpoint=_default_endpoint, tim
------------- -------------
https://api.hmny.io/#454b032c-6072-4ecb-bf24-38b3d6d2af69 https://api.hmny.io/#454b032c-6072-4ecb-bf24-38b3d6d2af69
""" """
method = 'hmyv2_getDelegationsByDelegator' method = "hmyv2_getDelegationsByDelegator"
params = [ params = [ delegator_addr ]
delegator_addr
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_delegations_by_delegator_by_block_number(delegator_addr, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
""" def get_delegations_by_delegator_by_block_number(
Get list of delegations by a delegator at a specific block delegator_addr,
block_num,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get list of delegations by a delegator at a specific block.
Parameters Parameters
---------- ----------
@ -515,20 +610,26 @@ def get_delegations_by_delegator_by_block_number(delegator_addr, block_num, endp
------------- -------------
https://api.hmny.io/#8ce13bda-e768-47b9-9dbe-193aba410b0a https://api.hmny.io/#8ce13bda-e768-47b9-9dbe-193aba410b0a
""" """
method = 'hmyv2_getDelegationsByDelegatorByBlockNumber' method = "hmyv2_getDelegationsByDelegatorByBlockNumber"
params = [ params = [ delegator_addr, block_num ]
delegator_addr,
block_num
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_delegation_by_delegator_and_validator(delegator_addr, validator_address,
endpoint=_default_endpoint, timeout=_default_timeout) -> dict: def get_delegation_by_delegator_and_validator(
""" delegator_addr,
Get list of delegations by a delegator at a specific block validator_address,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT,
) -> dict:
"""Get list of delegations by a delegator at a specific block.
Parameters Parameters
---------- ----------
@ -543,7 +644,8 @@ def get_delegation_by_delegator_and_validator(delegator_addr, validator_address,
Returns Returns
------- -------
one delegation (or None if such delegation doesn't exist), see get_delegations_by_delegator for fields one delegation (or None if such delegation doesn't exist)
see get_delegations_by_delegator for fields
Raises Raises
------ ------
@ -554,19 +656,25 @@ def get_delegation_by_delegator_and_validator(delegator_addr, validator_address,
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L605 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L605
""" """
method = 'hmyv2_getDelegationByDelegatorAndValidator' method = "hmyv2_getDelegationByDelegatorAndValidator"
params = [ params = [ delegator_addr, validator_address ]
delegator_addr,
validator_address
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_available_redelegation_balance(delegator_addr, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
""" def get_available_redelegation_balance(
Get amount of locked undelegated tokens delegator_addr,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get amount of locked undelegated tokens.
Parameters Parameters
---------- ----------
@ -590,18 +698,27 @@ def get_available_redelegation_balance(delegator_addr, endpoint=_default_endpoin
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L653 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L653
""" """
method = 'hmyv2_getAvailableRedelegationBalance' method = "hmyv2_getAvailableRedelegationBalance"
params = [ params = [ delegator_addr ]
delegator_addr
]
try: try:
return int(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']) return int(
except (KeyError, TypeError) as e: rpc_request(
raise InvalidRPCReplyError(method, endpoint) from e method,
params = params,
def get_delegations_by_validator(validator_addr, endpoint=_default_endpoint, timeout=_default_timeout) -> list: endpoint = endpoint,
""" timeout = timeout
Get list of delegations to a validator )[ "result" ]
)
except ( KeyError, TypeError ) as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_delegations_by_validator(
validator_addr,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get list of delegations to a validator.
Parameters Parameters
---------- ----------
@ -625,21 +742,27 @@ def get_delegations_by_validator(validator_addr, endpoint=_default_endpoint, tim
------------- -------------
https://api.hmny.io/#2e02d8db-8fec-41d9-a672-2c9862f63f39 https://api.hmny.io/#2e02d8db-8fec-41d9-a672-2c9862f63f39
""" """
method = 'hmyv2_getDelegationsByValidator' method = "hmyv2_getDelegationsByValidator"
params = [ params = [ validator_addr ]
validator_addr
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
######################## ########################
# Staking Network RPCs # # Staking Network RPCs #
######################## ########################
def get_current_utility_metrics(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: def get_current_utility_metrics(
""" endpoint = DEFAULT_ENDPOINT,
Get current utility metrics of network timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get current utility metrics of network.
Parameters Parameters
---------- ----------
@ -665,15 +788,20 @@ def get_current_utility_metrics(endpoint=_default_endpoint, timeout=_default_tim
------------- -------------
https://api.hmny.io/#78dd2d94-9ff1-4e0c-bbac-b4eec1cdf10b https://api.hmny.io/#78dd2d94-9ff1-4e0c-bbac-b4eec1cdf10b
""" """
method = 'hmyv2_getCurrentUtilityMetrics' method = "hmyv2_getCurrentUtilityMetrics"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_staking_network_info(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
""" def get_staking_network_info(
Get staking network information endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get staking network information.
Parameters Parameters
---------- ----------
@ -700,15 +828,20 @@ def get_staking_network_info(endpoint=_default_endpoint, timeout=_default_timeou
------------- -------------
https://api.hmny.io/#4a10fce0-2aa4-4583-bdcb-81ee0800993b https://api.hmny.io/#4a10fce0-2aa4-4583-bdcb-81ee0800993b
""" """
method = 'hmyv2_getStakingNetworkInfo' method = "hmyv2_getStakingNetworkInfo"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_super_committees(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
""" def get_super_committees(
Get voting committees for current & previous epoch endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get voting committees for current & previous epoch.
Parameters Parameters
---------- ----------
@ -753,15 +886,20 @@ def get_super_committees(endpoint=_default_endpoint, timeout=_default_timeout) -
------------- -------------
https://api.hmny.io/#8eef2fc4-92db-4610-a9cd-f7b75cfbd080 https://api.hmny.io/#8eef2fc4-92db-4610-a9cd-f7b75cfbd080
""" """
method = 'hmyv2_getSuperCommittees' method = "hmyv2_getSuperCommittees"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_total_staking(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
""" def get_total_staking(
Get total staking by validators, only meant to be called on beaconchain endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> int:
"""Get total staking by validators, only meant to be called on beaconchain.
Parameters Parameters
---------- ----------
@ -783,15 +921,22 @@ def get_total_staking(endpoint=_default_endpoint, timeout=_default_timeout) -> i
------------- -------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L102 https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L102
""" """
method = 'hmyv2_getTotalStaking' method = "hmyv2_getTotalStaking"
try: try:
return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result']) return int(
except (KeyError, TypeError) as e: rpc_request( method,
raise InvalidRPCReplyError(method, endpoint) from e endpoint = endpoint,
timeout = timeout )[ "result" ]
)
except ( KeyError, TypeError ) as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_raw_median_stake_snapshot(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
""" def get_raw_median_stake_snapshot(
Get median stake & additional committee data of the current epoch endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get median stake & additional committee data of the current epoch.
Parameters Parameters
---------- ----------
@ -826,8 +971,10 @@ def get_raw_median_stake_snapshot(endpoint=_default_endpoint, timeout=_default_t
------------- -------------
https://api.hmny.io/#bef93b3f-6763-4121-9c17-f0b0d9e5cc40 https://api.hmny.io/#bef93b3f-6763-4121-9c17-f0b0d9e5cc40
""" """
method = 'hmyv2_getMedianRawStakeSnapshot' method = "hmyv2_getMedianRawStakeSnapshot"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception

@ -1,34 +1,23 @@
from cytoolz import ( """
pipe, Sign Harmony staking transactions
dissoc, """
partial,
merge,
identity,
)
from hexbytes import ( import math
HexBytes
)
import rlp from decimal import Decimal
import math from functools import partial
from toolz import ( pipe, dissoc, merge, identity, )
from decimal import ( from hexbytes import HexBytes
Decimal
)
from eth_account.datastructures import ( import rlp
SignedTransaction
)
from eth_account._utils.signing import ( from eth_account.datastructures import SignedTransaction
sign_transaction_hash
)
from eth_account._utils.transactions import ( from eth_account._utils.signing import sign_transaction_hash
chain_id_to_v
) from eth_account._utils.legacy_transactions import chain_id_to_v
from eth_utils.curried import ( from eth_utils.curried import (
hexstr_if_str, hexstr_if_str,
@ -37,31 +26,30 @@ from eth_utils.curried import (
apply_formatters_to_dict, apply_formatters_to_dict,
to_int, to_int,
apply_formatters_to_sequence, apply_formatters_to_sequence,
apply_formatter_to_array apply_formatter_to_array,
) )
from .signing import ( from .constants import PRECISION, MAX_DECIMAL
sanitize_transaction
) from .signing import sanitize_transaction
from .staking_structures import ( from .staking_structures import (
FORMATTERS, FORMATTERS,
StakingSettings,
Directive, Directive,
CreateValidator, CreateValidator,
EditValidator, EditValidator,
DelegateOrUndelegate, DelegateOrUndelegate,
CollectRewards CollectRewards,
) )
from .util import ( from .util import convert_one_to_hex
convert_one_to_hex
)
def _convert_staking_percentage_to_number(value): # https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L335
""" # https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L335
Convert from staking percentage to integer def _convert_staking_percentage_to_number( value, ):
For example, 0.1 becomes 1000000000000000000 """Convert from staking percentage to integer For example, 0.1 becomes
1000000000000000000. Since Python floats are problematic with precision,
this function is used as a workaround.
Parameters Parameters
--------- ---------
@ -77,43 +65,45 @@ def _convert_staking_percentage_to_number(value): # https://github.com/ha
AssertionError, if data types are not as expected AssertionError, if data types are not as expected
ValueError, if the input type is not supported ValueError, if the input type is not supported
""" """
assert isinstance(value, (str, Decimal)), 'Only strings or decimals are supported' assert isinstance( value, ( str, Decimal ) ), "Only strings or decimals are supported"
if isinstance(value, Decimal): if isinstance( value, Decimal ):
value = str(value) value = str( value )
value1 = value; value1 = value
if value[0] == '-': if value[ 0 ] == "-":
raise ValueError('Negative numbers are not accepted') raise ValueError( "Negative numbers are not accepted" )
if value[0] == '+': if value[ 0 ] == "+":
value1 = value[1:] value1 = value[ 1 : ]
if len(value1) == 0: if len( value1 ) == 0:
raise ValueError('StakingDecimal string is empty') raise ValueError( "StakingDecimal string is empty" )
spaced = value1.split(' ') spaced = value1.split( " " )
if len(spaced) > 1: if len( spaced ) > 1:
raise ValueError('Bad decimal string') raise ValueError( "Bad decimal string" )
splitted = value1.split('.') splitted = value1.split( "." )
combined_str = splitted[0] combined_str = splitted[ 0 ]
if len(splitted) == 2: if len( splitted ) == 2:
length = len(splitted[1]) length = len( splitted[ 1 ] )
if length == 0 or len(combined_str) == 0: if length == 0 or len( combined_str ) == 0:
raise ValueError('Bad StakingDecimal length') raise ValueError( "Bad StakingDecimal length" )
if splitted[1][0] == '-': if splitted[ 1 ][ 0 ] == "-":
raise ValueError('Bad StakingDecimal string') raise ValueError( "Bad StakingDecimal string" )
combined_str += splitted[1] combined_str += splitted[ 1 ]
elif len(splitted) > 2: elif len( splitted ) > 2:
raise ValueError('Too many periods to be a StakingDecimal string') raise ValueError( "Too many periods to be a StakingDecimal string" )
if length > StakingSettings.PRECISION: if length > PRECISION:
raise ValueError('Too much precision, must be less than {StakingSettings.PRECISION}') raise ValueError( "Too much precision, must be less than {PRECISION}" )
zeroes_to_add = StakingSettings.PRECISION - length zeroes_to_add = PRECISION - length
combined_str += '0' * zeroes_to_add # This will not have any periods, so it is effectively a large integer combined_str += (
val = int(combined_str) "0" * zeroes_to_add
assert val <= StakingSettings.MAX_DECIMAL, 'Staking percentage is too large' ) # This will not have any periods, so it is effectively a large integer
val = int( combined_str )
assert val <= MAX_DECIMAL, "Staking percentage is too large"
return val return val
def _get_account_and_transaction(transaction_dict, private_key):
""" def _get_account_and_transaction( transaction_dict, private_key ):
Create account from private key and sanitize the transaction """Create account from private key and sanitize the transaction
Sanitization involves removal of 'from' key Sanitization involves removal of 'from' key And conversion of chainId key
And conversion of chainId key from str to int (if present) from str to int ( if present )
Parameters Parameters
---------- ----------
@ -132,18 +122,27 @@ def _get_account_and_transaction(transaction_dict, private_key):
AssertionError, if chainId is not present in util.chain_id_to_int AssertionError, if chainId is not present in util.chain_id_to_int
TypeError, if the value of 'from' key is not the same as account address TypeError, if the value of 'from' key is not the same as account address
""" """
account, sanitized_transaction = sanitize_transaction(transaction_dict, private_key) # remove from, convert chain id (if present) to integer account, sanitized_transaction = sanitize_transaction(
sanitized_transaction['directive'] = sanitized_transaction['directive'].value # convert to value, like in TypeScript transaction_dict, private_key
) # remove from, convert chain id ( if present ) to integer
sanitized_transaction[ "directive" ] = sanitized_transaction[
"directive" ].value # convert to value, like in TypeScript
return account, sanitized_transaction return account, sanitized_transaction
def _sign_transaction_generic(account, sanitized_transaction, parent_serializer):
""" # pylint: disable=too-many-locals,protected-access,invalid-name
Sign a generic staking transaction, given the serializer base class and account def _sign_transaction_generic(
account,
sanitized_transaction,
parent_serializer
):
"""Sign a generic staking transaction, given the serializer base class and
account.
Paramters Paramters
--------- ---------
account: :obj:`eth_account.Account`, the account to use for signing account: :obj:`eth_account.Account`, the account to use for signing
sanitized_transaction: :obj:`dict`, The sanitized transaction (chainId checks and no from key) sanitized_transaction: :obj:`dict`, The sanitized transaction ( chainId checks and no from key )
parent_serializer: :obj: The serializer class from staking_structures parent_serializer: :obj: The serializer class from staking_structures
Returns Returns
@ -157,183 +156,283 @@ def _sign_transaction_generic(account, sanitized_transaction, parent_serializer)
rlp.exceptions.ObjectSerializationError, if data types are not as expected rlp.exceptions.ObjectSerializationError, if data types are not as expected
""" """
# obtain the serializers # obtain the serializers
if sanitized_transaction.get('chainId', 0) == 0: if sanitized_transaction.get( "chainId", 0 ) == 0:
unsigned_serializer, signed_serializer = parent_serializer.Unsigned(), parent_serializer.Signed() # unsigned, signed unsigned_serializer, signed_serializer = (
parent_serializer.Unsigned( ),
parent_serializer.Signed( ),
) # unsigned, signed
else: else:
unsigned_serializer, signed_serializer = parent_serializer.SignedChainId(), parent_serializer.SignedChainId() # since chain_id_to_v adds v/r/s, unsigned is not used here unsigned_serializer, signed_serializer = (
parent_serializer.SignedChainId( ),
parent_serializer.SignedChainId( ),
) # since chain_id_to_v adds v/r/s, unsigned is not used here
# fill the transaction # fill the transaction
filled_transaction = pipe( # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L39 # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L39
filled_transaction = pipe(
sanitized_transaction, sanitized_transaction,
dict, dict,
partial(merge, {'chainId': None}), partial( merge, { "chainId": None } ),
chain_id_to_v, # will move chain id to v and add v/r/s chain_id_to_v, # will move chain id to v and add v/r/s
apply_formatters_to_dict(FORMATTERS) apply_formatters_to_dict( FORMATTERS ),
) )
# get the unsigned transaction # get the unsigned transaction
for f, _ in unsigned_serializer._meta.fields: for field, _ in unsigned_serializer._meta.fields:
assert f in filled_transaction, f'Could not find {f} in transaction' assert field in filled_transaction, f"Could not find {field} in transaction"
unsigned_transaction = unsigned_serializer.from_dict(\ unsigned_transaction = unsigned_serializer.from_dict(
{f: filled_transaction[f] for f, _ in unsigned_serializer._meta.fields}) # drop extras silently {
f: filled_transaction[ f ]
for f,
_ in unsigned_serializer._meta.fields
}
) # drop extras silently
# sign the unsigned transaction # sign the unsigned transaction
if 'v' in unsigned_transaction.as_dict(): if "v" in unsigned_transaction.as_dict():
chain_id = unsigned_transaction.v chain_id = unsigned_transaction.v
else: else:
chain_id = None chain_id = None
transaction_hash = unsigned_transaction.hash() transaction_hash = unsigned_transaction.hash()
(v, r, s) = sign_transaction_hash( ( v,
account._key_obj, transaction_hash, chain_id) r,
s
) = sign_transaction_hash( account._key_obj,
transaction_hash,
chain_id )
chain_naive_transaction = dissoc( chain_naive_transaction = dissoc(
unsigned_transaction.as_dict(), 'v', 'r', 's') # remove extra v/r/s added by chain_id_to_v unsigned_transaction.as_dict(),
"v",
"r",
"s"
) # remove extra v/r/s added by chain_id_to_v
# serialize it # serialize it
# https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L207
signed_transaction = signed_serializer( signed_transaction = signed_serializer(
v=v + (8 if chain_id is None else 0), # copied from https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L207 v=v
+ (
8 if chain_id is None else 0
),
r=r, r=r,
s=s, # in the below statement, remove everything not expected by signed_serializer s=s, # in the below statement, remove everything not expected by signed_serializer
**{f: chain_naive_transaction[f] for f, _ in signed_serializer._meta.fields if f not in 'vrs'}) **{
f: chain_naive_transaction[ f ]
for f, _ in signed_serializer._meta.fields
if f not in "vrs"
},
)
# encode it # encode it
encoded_transaction = rlp.encode(signed_transaction) encoded_transaction = rlp.encode( signed_transaction )
# hash it # hash it
signed_transaction_hash = keccak(encoded_transaction) signed_transaction_hash = keccak( encoded_transaction )
# return is # return is
return SignedTransaction( return SignedTransaction(
rawTransaction=HexBytes(encoded_transaction), rawTransaction = HexBytes( encoded_transaction ),
hash=HexBytes(signed_transaction_hash), hash = HexBytes( signed_transaction_hash ),
r=r, r = r,
s=s, s = s,
v=v, v = v,
) )
def _sign_delegate_or_undelegate(transaction_dict, private_key, delegate):
""" def _sign_delegate_or_undelegate( transaction_dict, private_key ):
Sign a delegate or undelegate transaction """Sign a delegate or undelegate transaction See sign_staking_transaction
See sign_staking_transaction for details for details."""
"""
# preliminary steps # preliminary steps
if transaction_dict['directive'] not in [ Directive.Delegate, Directive.Undelegate ]: if transaction_dict[ "directive" ] not in [
raise TypeError('Only Delegate or Undelegate are supported by _sign_delegate_or_undelegate') Directive.Delegate,
Directive.Undelegate
]:
raise TypeError(
"Only Delegate or Undelegate are supported by _sign_delegate_or_undelegate"
)
# first common step # first common step
account, sanitized_transaction = _get_account_and_transaction(transaction_dict, private_key) account, sanitized_transaction = _get_account_and_transaction(
transaction_dict, private_key
)
# encode the stakeMsg # encode the stakeMsg
sanitized_transaction['stakeMsg'] = \ sanitized_transaction[ "stakeMsg" ] = apply_formatters_to_sequence(
apply_formatters_to_sequence( [ [
hexstr_if_str(to_bytes), hexstr_if_str( to_bytes ),
hexstr_if_str(to_bytes), hexstr_if_str( to_bytes ),
hexstr_if_str(to_int) hexstr_if_str( to_int )
], [ ],
convert_one_to_hex(sanitized_transaction.pop('delegatorAddress')), [
convert_one_to_hex(sanitized_transaction.pop('validatorAddress')), convert_one_to_hex(
sanitized_transaction.pop('amount'), sanitized_transaction.pop( "delegatorAddress" )
] ),
convert_one_to_hex(
sanitized_transaction.pop( "validatorAddress" )
),
sanitized_transaction.pop( "amount" ),
],
)
return _sign_transaction_generic(
account,
sanitized_transaction,
DelegateOrUndelegate
) )
return _sign_transaction_generic(account, sanitized_transaction, DelegateOrUndelegate)
def _sign_collect_rewards(transaction_dict, private_key):
""" def _sign_collect_rewards( transaction_dict, private_key ):
Sign a collect rewards transaction """Sign a collect rewards transaction See sign_staking_transaction for
See sign_staking_transaction for details details."""
"""
# preliminary steps # preliminary steps
if transaction_dict['directive'] != Directive.CollectRewards: if transaction_dict[ "directive" ] != Directive.CollectRewards:
raise TypeError('Only CollectRewards is supported by _sign_collect_rewards') raise TypeError(
"Only CollectRewards is supported by _sign_collect_rewards"
)
# first common step # first common step
account, sanitized_transaction = _get_account_and_transaction(transaction_dict, private_key) account, sanitized_transaction = _get_account_and_transaction(
transaction_dict, private_key
)
# encode the stakeMsg # encode the stakeMsg
sanitized_transaction['stakeMsg'] = \ sanitized_transaction[ "stakeMsg" ] = [
[hexstr_if_str(to_bytes)(convert_one_to_hex(sanitized_transaction.pop('delegatorAddress')))] hexstr_if_str( to_bytes )(
return _sign_transaction_generic(account, sanitized_transaction, CollectRewards) convert_one_to_hex(
sanitized_transaction.pop( "delegatorAddress" )
)
)
]
return _sign_transaction_generic(
account,
sanitized_transaction,
CollectRewards
)
def _sign_create_validator(transaction_dict, private_key):
""" def _sign_create_validator( transaction_dict, private_key ):
Sign a create validator transaction """Sign a create validator transaction See sign_staking_transaction for
See sign_staking_transaction for details details."""
"""
# preliminary steps # preliminary steps
if transaction_dict['directive'] != Directive.CreateValidator: if transaction_dict[ "directive" ] != Directive.CreateValidator:
raise TypeError('Only CreateValidator is supported by _sign_create_or_edit_validator') raise TypeError(
"Only CreateValidator is supported by _sign_create_or_edit_validator"
)
# first common step # first common step
account, sanitized_transaction = _get_account_and_transaction(transaction_dict, private_key) account, sanitized_transaction = _get_account_and_transaction(
transaction_dict, private_key
)
# encode the stakeMsg # encode the stakeMsg
description = [ description = [
sanitized_transaction.pop('name'), sanitized_transaction.pop( "name" ),
sanitized_transaction.pop('identity'), sanitized_transaction.pop( "identity" ),
sanitized_transaction.pop('website'), sanitized_transaction.pop( "website" ),
sanitized_transaction.pop('security-contact'), sanitized_transaction.pop( "security-contact" ),
sanitized_transaction.pop('details'), sanitized_transaction.pop( "details" ),
] ]
commission = apply_formatter_to_array( hexstr_if_str(to_int), # formatter commission = apply_formatter_to_array(
hexstr_if_str( to_int ), # formatter
[ [
_convert_staking_percentage_to_number(sanitized_transaction.pop('rate')), _convert_staking_percentage_to_number( sanitized_transaction.pop( "rate" ) ),
_convert_staking_percentage_to_number(sanitized_transaction.pop('max-rate')), _convert_staking_percentage_to_number(
_convert_staking_percentage_to_number(sanitized_transaction.pop('max-change-rate')), sanitized_transaction.pop( "max-rate" )
] ),
_convert_staking_percentage_to_number(
sanitized_transaction.pop( "max-change-rate" )
),
],
)
commission = [ [ element ] for element in commission ]
bls_keys = apply_formatter_to_array(
hexstr_if_str( to_bytes ), # formatter
sanitized_transaction.pop( "bls-public-keys" ),
) )
commission = [ [element] for element in commission ] bls_key_sigs = apply_formatter_to_array(
bls_keys = apply_formatter_to_array( hexstr_if_str(to_bytes), # formatter hexstr_if_str( to_bytes ),
sanitized_transaction.pop('bls-public-keys') sanitized_transaction.pop( "bls-key-sigs" ) # formatter
) )
sanitized_transaction['stakeMsg'] = \ sanitized_transaction[ "stakeMsg" ] = apply_formatters_to_sequence(
apply_formatters_to_sequence( [ [
hexstr_if_str(to_bytes), # address hexstr_if_str( to_bytes ), # address
identity, # description identity, # description
identity, # commission rates identity, # commission rates
hexstr_if_str(to_int), # min self delegation (in ONE), decimals are silently dropped hexstr_if_str(
hexstr_if_str(to_int), # max total delegation (in ONE), decimals are silently dropped to_int
), # min self delegation ( in ONE ), decimals are silently dropped
hexstr_if_str(
to_int
), # max total delegation ( in ONE ), decimals are silently dropped
identity, # bls public keys identity, # bls public keys
hexstr_if_str(to_int), # amount (the Hexlify in the SDK drops the decimals, which is what we will do too) identity, # bls key sigs
], [ hexstr_if_str(
convert_one_to_hex(sanitized_transaction.pop('validatorAddress')), to_int
), # amount ( the Hexlify in the SDK drops the decimals, which is what we will do too )
],
[
convert_one_to_hex( sanitized_transaction.pop( "validatorAddress" ) ),
description, description,
commission, commission,
math.floor(sanitized_transaction.pop('min-self-delegation')), # Decimal floors it correctly math.floor(
math.floor(sanitized_transaction.pop('max-total-delegation')), sanitized_transaction.pop( "min-self-delegation" )
), # Decimal floors it correctly
math.floor( sanitized_transaction.pop( "max-total-delegation" ) ),
bls_keys, bls_keys,
math.floor(sanitized_transaction.pop('amount')), bls_key_sigs,
] math.floor( sanitized_transaction.pop( "amount" ) ),
],
)
return _sign_transaction_generic(
account,
sanitized_transaction,
CreateValidator
) )
return _sign_transaction_generic(account, sanitized_transaction, CreateValidator)
def _sign_edit_validator(transaction_dict, private_key):
""" def _sign_edit_validator( transaction_dict, private_key ):
Sign an edit validator transaction """Sign an edit validator transaction See sign_staking_transaction for
See sign_staking_transaction for details details."""
"""
# preliminary steps # preliminary steps
if transaction_dict['directive'] != Directive.EditValidator: if transaction_dict[ "directive" ] != Directive.EditValidator:
raise TypeError('Only EditValidator is supported by _sign_create_or_edit_validator') raise TypeError(
"Only EditValidator is supported by _sign_create_or_edit_validator"
)
# first common step # first common step
account, sanitized_transaction = _get_account_and_transaction(transaction_dict, private_key) account, sanitized_transaction = _get_account_and_transaction(
transaction_dict, private_key
)
# encode the stakeMsg # encode the stakeMsg
description = [ description = [
sanitized_transaction.pop('name'), sanitized_transaction.pop( "name" ),
sanitized_transaction.pop('identity'), sanitized_transaction.pop( "identity" ),
sanitized_transaction.pop('website'), sanitized_transaction.pop( "website" ),
sanitized_transaction.pop('security-contact'), sanitized_transaction.pop( "security-contact" ),
sanitized_transaction.pop('details'), sanitized_transaction.pop( "details" ),
] ]
sanitized_transaction['stakeMsg'] = \ sanitized_transaction[ "stakeMsg" ] = apply_formatters_to_sequence(
apply_formatters_to_sequence( [ [
hexstr_if_str(to_bytes), # address hexstr_if_str( to_bytes ), # address
identity, # description identity, # description
identity, # new rate (it's in a list so can't do hexstr_if_str) identity, # new rate ( it's in a list so can't do hexstr_if_str )
hexstr_if_str(to_int), # min self delegation (in ONE), decimals are silently dropped hexstr_if_str(
hexstr_if_str(to_int), # max total delegation (in ONE), decimals are silently dropped to_int
hexstr_if_str(to_bytes), # key to remove ), # min self delegation ( in ONE ), decimals are silently dropped
hexstr_if_str(to_bytes), # key to add hexstr_if_str(
], [ to_int
convert_one_to_hex(sanitized_transaction.pop('validatorAddress')), ), # max total delegation ( in ONE ), decimals are silently dropped
hexstr_if_str( to_bytes ), # key to remove
hexstr_if_str( to_bytes ), # key to add
hexstr_if_str( to_bytes ), # key to add sig
],
[
convert_one_to_hex( sanitized_transaction.pop( "validatorAddress" ) ),
description, description,
[ _convert_staking_percentage_to_number(sanitized_transaction.pop('rate')) ], [ _convert_staking_percentage_to_number( sanitized_transaction.pop( "rate" ) ) ],
math.floor(sanitized_transaction.pop('min-self-delegation')), # Decimal floors it correctly math.floor(
math.floor(sanitized_transaction.pop('max-total-delegation')), sanitized_transaction.pop( "min-self-delegation" )
sanitized_transaction.pop('bls-key-to-remove'), ), # Decimal floors it correctly
sanitized_transaction.pop('bls-key-to-add') math.floor( sanitized_transaction.pop( "max-total-delegation" ) ),
] sanitized_transaction.pop( "bls-key-to-remove" ),
sanitized_transaction.pop( "bls-key-to-add" ),
sanitized_transaction.pop( "bls-key-to-add-sig" ),
],
)
return _sign_transaction_generic(
account,
sanitized_transaction,
EditValidator
) )
return _sign_transaction_generic(account, sanitized_transaction, EditValidator)
def sign_staking_transaction(transaction_dict, private_key):
""" def sign_staking_transaction( transaction_dict, private_key ):
Sign a supplied transaction_dict with the private_key """Sign a supplied transaction_dict with the private_key.
Parameters Parameters
---------- ----------
@ -350,7 +449,7 @@ def sign_staking_transaction(transaction_dict, private_key):
Delegate/Undelegate: Delegate/Undelegate:
delegatorAddress: :obj:`str`, Address of the delegator delegatorAddress: :obj:`str`, Address of the delegator
validatorAddress: :obj:`str`, Address of the validator validatorAddress: :obj:`str`, Address of the validator
amount: :obj:`int`, Amount to (un)delegate in ATTO amount: :obj:`int`, Amount to ( un )delegate in ATTO
CreateValidator: CreateValidator:
validatorAddress: :obj:`str`, Address of the validator validatorAddress: :obj:`str`, Address of the validator
name: ;obj:`str`, Name of the validator name: ;obj:`str`, Name of the validator
@ -399,16 +498,23 @@ def sign_staking_transaction(transaction_dict, private_key):
------------- -------------
https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts
""" """
assert isinstance(transaction_dict, dict), 'Only dictionaries are supported' # OrderedDict is a subclass assert isinstance(
assert 'directive' in transaction_dict, 'Staking transaction type not specified' transaction_dict, dict
assert isinstance(transaction_dict['directive'], Directive), 'Unknown staking transaction type' ), "Only dictionaries are supported" # OrderedDict is a subclass
if transaction_dict['directive'] == Directive.CollectRewards: # chain_id missing => results in rlp decoding error for GasLimit
return _sign_collect_rewards(transaction_dict, private_key) assert "chainId" in transaction_dict, "chainId missing"
elif transaction_dict['directive'] == Directive.Delegate: assert "directive" in transaction_dict, "Staking transaction type not specified"
return _sign_delegate_or_undelegate(transaction_dict, private_key, True) assert isinstance(
elif transaction_dict['directive'] == Directive.Undelegate: transaction_dict[ "directive" ], Directive
return _sign_delegate_or_undelegate(transaction_dict, private_key, False) ), "Unknown staking transaction type"
elif transaction_dict['directive'] == Directive.CreateValidator: if transaction_dict[ "directive" ] == Directive.CollectRewards:
return _sign_create_validator(transaction_dict, private_key) return _sign_collect_rewards( transaction_dict, private_key )
elif transaction_dict['directive'] == Directive.EditValidator: if transaction_dict[ "directive" ] == Directive.Delegate:
return _sign_edit_validator(transaction_dict, private_key) return _sign_delegate_or_undelegate( transaction_dict, private_key )
if transaction_dict[ "directive" ] == Directive.Undelegate:
return _sign_delegate_or_undelegate( transaction_dict, private_key )
if transaction_dict[ "directive" ] == Directive.CreateValidator:
return _sign_create_validator( transaction_dict, private_key )
if transaction_dict[ "directive" ] == Directive.EditValidator:
return _sign_edit_validator( transaction_dict, private_key )
raise ValueError( 'Unknown staking transaction type' )

@ -1,218 +1,325 @@
from enum import ( """
Enum, Helper module for signing Harmony staking transactions
auto """
) # disable most of the Lint here
# pylint: disable=protected-access,no-member,invalid-name,missing-class-docstring,missing-function-docstring
from rlp.sedes import ( from enum import Enum, auto
big_endian_int,
Binary, from rlp.sedes import big_endian_int, Binary, CountableList, List, Text
CountableList,
List, from eth_rlp import HashableRLP
Text
) from eth_utils.curried import ( to_int, hexstr_if_str, )
from eth_rlp import (
HashableRLP # https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L120
) class Directive( Enum ):
def _generate_next_value_( name, start, count, last_values ): # pylint: disable=no-self-argument
from eth_utils.curried import (
to_int,
hexstr_if_str,
)
class StakingSettings:
PRECISION = 18
MAX_DECIMAL = 1000000000000000000
class Directive(Enum): # https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L120
def _generate_next_value_(name, start, count, last_values):
return count return count
CreateValidator = auto() CreateValidator = auto()
EditValidator = auto() EditValidator = auto()
Delegate = auto() Delegate = auto()
Undelegate = auto() Undelegate = auto()
CollectRewards = auto() CollectRewards = auto()
FORMATTERS = { FORMATTERS = {
'directive': hexstr_if_str(to_int), # delegatorAddress is already formatted before the call "directive": hexstr_if_str(
'nonce': hexstr_if_str(to_int), to_int
'gasPrice': hexstr_if_str(to_int), ), # delegatorAddress is already formatted before the call
'gasLimit': hexstr_if_str(to_int), "nonce": hexstr_if_str(to_int),
'chainId': hexstr_if_str(to_int), "gasPrice": hexstr_if_str(to_int),
"gasLimit": hexstr_if_str(to_int),
"chainId": hexstr_if_str(to_int),
} }
class CollectRewards: class CollectRewards:
@staticmethod @staticmethod
def UnsignedChainId(): def UnsignedChainId():
class UnsignedChainId(HashableRLP): class UnsignedChainId( HashableRLP ):
fields = ( fields = (
('directive', big_endian_int), ( "directive",
('stakeMsg', CountableList(Binary.fixed_length(20, allow_empty=True))), big_endian_int ),
('nonce', big_endian_int), (
('gasPrice', big_endian_int), "stakeMsg",
('gasLimit', big_endian_int), CountableList(
('chainId', big_endian_int), Binary.fixed_length( 20,
allow_empty = True )
)
),
( "nonce",
big_endian_int ),
( "gasPrice",
big_endian_int ),
( "gasLimit",
big_endian_int ),
( "chainId",
big_endian_int ),
) )
return UnsignedChainId return UnsignedChainId
@staticmethod @staticmethod
def SignedChainId(): def SignedChainId():
class SignedChainId(HashableRLP): class SignedChainId( HashableRLP ):
fields = CollectRewards.UnsignedChainId()._meta.fields[:-1] + ( # drop chainId fields = CollectRewards.UnsignedChainId()._meta.fields[
:-1
] + ( # drop chainId
("v", big_endian_int), ("v", big_endian_int),
("r", big_endian_int), ("r", big_endian_int),
("s", big_endian_int), ("s", big_endian_int),
) )
return SignedChainId return SignedChainId
@staticmethod @staticmethod
def Unsigned(): def Unsigned():
class Unsigned(HashableRLP): class Unsigned( HashableRLP ):
fields = CollectRewards.UnsignedChainId()._meta.fields[:-1] # drop chainId fields = CollectRewards.UnsignedChainId(
)._meta.fields[ :-1 ] # drop chainId
return Unsigned return Unsigned
@staticmethod @staticmethod
def Signed(): def Signed():
class Signed(HashableRLP): class Signed( HashableRLP ):
fields = CollectRewards.Unsigned()._meta.fields[:-3] + ( # drop last 3 for raw.pop() fields = CollectRewards.Unsigned()._meta.fields[
:-3
] + ( # drop last 3 for raw.pop()
("v", big_endian_int), ("v", big_endian_int),
("r", big_endian_int), ("r", big_endian_int),
("s", big_endian_int), ("s", big_endian_int),
) )
return Signed return Signed
class DelegateOrUndelegate: class DelegateOrUndelegate:
@staticmethod @staticmethod
def UnsignedChainId(): def UnsignedChainId():
class UnsignedChainId(HashableRLP): class UnsignedChainId( HashableRLP ):
fields = ( fields = (
('directive', big_endian_int), ( "directive",
('stakeMsg', List([Binary.fixed_length(20, allow_empty=True),Binary.fixed_length(20, allow_empty=True),big_endian_int],True)), big_endian_int ),
('nonce', big_endian_int), (
('gasPrice', big_endian_int), "stakeMsg",
('gasLimit', big_endian_int), List(
('chainId', big_endian_int), [
Binary.fixed_length( 20,
allow_empty = True ),
Binary.fixed_length( 20,
allow_empty = True ),
big_endian_int,
],
True,
),
),
( "nonce",
big_endian_int ),
( "gasPrice",
big_endian_int ),
( "gasLimit",
big_endian_int ),
( "chainId",
big_endian_int ),
) )
return UnsignedChainId return UnsignedChainId
@staticmethod @staticmethod
def SignedChainId(): def SignedChainId():
class SignedChainId(HashableRLP): class SignedChainId( HashableRLP ):
fields = DelegateOrUndelegate.UnsignedChainId()._meta.fields[:-1] + ( # drop chainId fields = DelegateOrUndelegate.UnsignedChainId()._meta.fields[
:-1
] + ( # drop chainId
("v", big_endian_int), ("v", big_endian_int),
("r", big_endian_int), ("r", big_endian_int),
("s", big_endian_int), ("s", big_endian_int),
) )
return SignedChainId return SignedChainId
@staticmethod @staticmethod
def Unsigned(): def Unsigned():
class Unsigned(HashableRLP): class Unsigned( HashableRLP ):
fields = DelegateOrUndelegate.UnsignedChainId()._meta.fields[:-1] # drop chainId fields = DelegateOrUndelegate.UnsignedChainId(
)._meta.fields[ :-1 ] # drop chainId
return Unsigned return Unsigned
@staticmethod @staticmethod
def Signed(): def Signed():
class Signed(HashableRLP): class Signed( HashableRLP ):
fields = DelegateOrUndelegate.Unsigned()._meta.fields[:-3] + ( # drop last 3 for raw.pop() fields = DelegateOrUndelegate.Unsigned()._meta.fields[
:-3
] + ( # drop last 3 for raw.pop()
("v", big_endian_int), ("v", big_endian_int),
("r", big_endian_int), ("r", big_endian_int),
("s", big_endian_int), ("s", big_endian_int),
) )
return Signed return Signed
class CreateValidator: class CreateValidator:
@staticmethod @staticmethod
def UnsignedChainId(): def UnsignedChainId():
class UnsignedChainId(HashableRLP): class UnsignedChainId( HashableRLP ):
fields = ( fields = (
('directive', big_endian_int), ("directive", big_endian_int),
('stakeMsg', List([ # list with the following members (
Binary.fixed_length(20, allow_empty=True), # validatorAddress "stakeMsg",
List([Text()]*5,True), # description is Text of 5 elements List(
List([List([big_endian_int],True)]*3,True), # commission rate is made up of 3 integers in an array [ [int1], [int2], [int3] ] [ # list with the following members
# validatorAddress
Binary.fixed_length(
20, allow_empty=True
),
# description is Text of 5 elements
List(
[Text()] * 5, True
),
# commission rate is made up of 3 integers in an array
List(
[List([big_endian_int], True)] * 3, True
),
big_endian_int, # min self delegation big_endian_int, # min self delegation
big_endian_int, # max total delegation big_endian_int, # max total delegation
CountableList(Binary.fixed_length(48, allow_empty=True)), # bls-public-keys array of unspecified length, each key of 48 # bls-public-keys array of unspecified length, each key of 48
CountableList(
Binary.fixed_length(48, allow_empty=True)
),
# bls-key-sigs array of unspecified length, each sig of 96
CountableList(
Binary.fixed_length(96, allow_empty=True)
),
big_endian_int, # amount big_endian_int, # amount
], True)), # strictly these number of elements ],
('nonce', big_endian_int), True,
('gasPrice', big_endian_int), ),
('gasLimit', big_endian_int), ), # strictly these number of elements
('chainId', big_endian_int), ("nonce", big_endian_int),
("gasPrice", big_endian_int),
("gasLimit", big_endian_int),
("chainId", big_endian_int),
) )
return UnsignedChainId return UnsignedChainId
@staticmethod @staticmethod
def SignedChainId(): def SignedChainId():
class SignedChainId(HashableRLP): class SignedChainId( HashableRLP ):
fields = CreateValidator.UnsignedChainId()._meta.fields[:-1] + ( # drop chainId fields = CreateValidator.UnsignedChainId()._meta.fields[
:-1
] + ( # drop chainId
("v", big_endian_int), ("v", big_endian_int),
("r", big_endian_int), ("r", big_endian_int),
("s", big_endian_int), ("s", big_endian_int),
) )
return SignedChainId return SignedChainId
@staticmethod @staticmethod
def Unsigned(): def Unsigned():
class Unsigned(HashableRLP): class Unsigned( HashableRLP ):
fields = CreateValidator.UnsignedChainId()._meta.fields[:-1] # drop chainId fields = CreateValidator.UnsignedChainId(
)._meta.fields[ :-1 ] # drop chainId
return Unsigned return Unsigned
@staticmethod @staticmethod
def Signed(): def Signed():
class Signed(HashableRLP): class Signed( HashableRLP ):
fields = CreateValidator.Unsigned()._meta.fields[:-3] + ( # drop last 3 for raw.pop() fields = CreateValidator.Unsigned()._meta.fields[
:-3
] + ( # drop last 3 for raw.pop()
("v", big_endian_int), ("v", big_endian_int),
("r", big_endian_int), ("r", big_endian_int),
("s", big_endian_int), ("s", big_endian_int),
) )
return Signed return Signed
class EditValidator: class EditValidator:
@staticmethod @staticmethod
def UnsignedChainId(): def UnsignedChainId():
class UnsignedChainId(HashableRLP): class UnsignedChainId( HashableRLP ):
fields = ( fields = (
('directive', big_endian_int), ("directive", big_endian_int),
('stakeMsg', List([ # list with the following members (
Binary.fixed_length(20, allow_empty=True), # validatorAddress "stakeMsg",
List([Text()]*5,True), # description is Text of 5 elements List(
List([big_endian_int],True), # new rate is in a list [ # list with the following members
# validatorAddress
Binary.fixed_length(
20, allow_empty=True
),
# description is Text of 5 elements
List(
[Text()] * 5, True
),
# new rate is in a list
List([big_endian_int], True),
big_endian_int, # min self delegation big_endian_int, # min self delegation
big_endian_int, # max total delegation big_endian_int, # max total delegation
Binary.fixed_length(48, allow_empty=True), # slot key to remove # slot key to remove
Binary.fixed_length(48, allow_empty=True), # slot key to add Binary.fixed_length(
], True)), # strictly these number of elements 48, allow_empty=True
('nonce', big_endian_int), ),
('gasPrice', big_endian_int), # slot key to add
('gasLimit', big_endian_int), Binary.fixed_length(
('chainId', big_endian_int), 48, allow_empty=True
),
# slot key to add sig
Binary.fixed_length(
96, allow_empty=True
),
],
True,
),
), # strictly these number of elements
("nonce", big_endian_int),
("gasPrice", big_endian_int),
("gasLimit", big_endian_int),
("chainId", big_endian_int),
) )
return UnsignedChainId return UnsignedChainId
@staticmethod @staticmethod
def SignedChainId(): def SignedChainId():
class SignedChainId(HashableRLP): class SignedChainId( HashableRLP ):
fields = EditValidator.UnsignedChainId()._meta.fields[:-1] + ( # drop chainId fields = EditValidator.UnsignedChainId()._meta.fields[
:-1
] + ( # drop chainId
("v", big_endian_int), ("v", big_endian_int),
("r", big_endian_int), ("r", big_endian_int),
("s", big_endian_int), ("s", big_endian_int),
) )
return SignedChainId return SignedChainId
@staticmethod @staticmethod
def Unsigned(): def Unsigned():
class Unsigned(HashableRLP): class Unsigned( HashableRLP ):
fields = EditValidator.UnsignedChainId()._meta.fields[:-1] # drop chainId fields = EditValidator.UnsignedChainId(
)._meta.fields[ :-1 ] # drop chainId
return Unsigned return Unsigned
@staticmethod @staticmethod
def Signed(): def Signed():
class Signed(HashableRLP): class Signed( HashableRLP ):
fields = EditValidator.Unsigned()._meta.fields[:-3] + ( # drop last 3 for raw.pop() fields = EditValidator.Unsigned()._meta.fields[
:-3
] + ( # drop last 3 for raw.pop()
("v", big_endian_int), ("v", big_endian_int),
("r", big_endian_int), ("r", big_endian_int),
("s", big_endian_int), ("s", big_endian_int),
) )
return Signed return Signed

@ -1,22 +1,22 @@
from .rpc.request import ( """
rpc_request Interact with Harmony's transaction RPC API
) """
from .exceptions import (
TxConfirmationTimedoutError
)
import time import time
import random import random
from .constants import DEFAULT_ENDPOINT, DEFAULT_TIMEOUT
_default_endpoint = 'http://localhost:9500' from .rpc.request import rpc_request
_default_timeout = 30 from .exceptions import TxConfirmationTimedoutError, InvalidRPCReplyError
######################### #########################
# Transaction Pool RPCs # # Transaction Pool RPCs #
######################### #########################
def get_pending_transactions(endpoint=_default_endpoint, timeout=_default_timeout) -> list: def get_pending_transactions(
""" endpoint = DEFAULT_ENDPOINT,
Get list of pending transactions timeout = DEFAULT_TIMEOUT
) -> list:
"""Get list of pending transactions.
Parameters Parameters
---------- ----------
@ -38,15 +38,20 @@ def get_pending_transactions(endpoint=_default_endpoint, timeout=_default_timeou
------------- -------------
https://api.hmny.io/#de6c4a12-fa42-44e8-972f-801bfde1dd18 https://api.hmny.io/#de6c4a12-fa42-44e8-972f-801bfde1dd18
""" """
method = 'hmyv2_pendingTransactions' method = "hmyv2_pendingTransactions"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_transaction_error_sink(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
""" def get_transaction_error_sink(
Get current transactions error sink endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get current transactions error sink.
Parameters Parameters
---------- ----------
@ -71,15 +76,20 @@ def get_transaction_error_sink(endpoint=_default_endpoint, timeout=_default_time
------------- -------------
https://api.hmny.io/#9aedbc22-6262-44b1-8276-cd8ae19fa600 https://api.hmny.io/#9aedbc22-6262-44b1-8276-cd8ae19fa600
""" """
method = 'hmyv2_getCurrentTransactionErrorSink' method = "hmyv2_getCurrentTransactionErrorSink"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_pending_staking_transactions(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
""" def get_pending_staking_transactions(
Get list of pending staking transactions endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Get list of pending staking transactions.
Parameters Parameters
---------- ----------
@ -101,16 +111,20 @@ def get_pending_staking_transactions(endpoint=_default_endpoint, timeout=_defaul
------------- -------------
https://api.hmny.io/#de0235e4-f4c9-4a69-b6d2-b77dc1ba7b12 https://api.hmny.io/#de0235e4-f4c9-4a69-b6d2-b77dc1ba7b12
""" """
method = 'hmyv2_pendingStakingTransactions' method = "hmyv2_pendingStakingTransactions"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_staking_transaction_error_sink(endpoint=_default_endpoint, timeout=_default_timeout) -> list: def get_staking_transaction_error_sink(
""" endpoint = DEFAULT_ENDPOINT,
Get current staking transactions error sink timeout = DEFAULT_TIMEOUT
) -> list:
"""Get current staking transactions error sink.
Parameters Parameters
---------- ----------
@ -136,15 +150,21 @@ def get_staking_transaction_error_sink(endpoint=_default_endpoint, timeout=_defa
------------- -------------
https://api.hmny.io/#bdd00e0f-2ba0-480e-b996-2ef13f10d75a https://api.hmny.io/#bdd00e0f-2ba0-480e-b996-2ef13f10d75a
""" """
method = 'hmyv2_getCurrentStakingErrorSink' method = "hmyv2_getCurrentStakingErrorSink"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_pool_stats(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
""" def get_pool_stats(
Get stats of the pool, that is, number of pending and queued (non-executable) transactions endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get stats of the pool, that is, number of pending and queued (non-
executable) transactions.
Parameters Parameters
---------- ----------
@ -168,18 +188,24 @@ def get_pool_stats(endpoint=_default_endpoint, timeout=_default_timeout) -> dict
------------- -------------
https://api.hmny.io/#7c2b9395-8f5e-4eb5-a687-2f1be683d83e https://api.hmny.io/#7c2b9395-8f5e-4eb5-a687-2f1be683d83e
""" """
method = 'hmyv2_getPoolStats' method = "hmyv2_getPoolStats"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
#################### ####################
# Transaction RPCs # # Transaction RPCs #
#################### ####################
def get_transaction_by_hash(tx_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> dict: def get_transaction_by_hash(
""" tx_hash,
Get transaction by hash endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get transaction by hash.
Parameters Parameters
---------- ----------
@ -223,20 +249,27 @@ def get_transaction_by_hash(tx_hash, endpoint=_default_endpoint, timeout=_defaul
------------- -------------
https://api.hmny.io/#117e84f6-a0ec-444e-abe0-455701310389 https://api.hmny.io/#117e84f6-a0ec-444e-abe0-455701310389
""" """
method = 'hmyv2_getTransactionByHash' method = "hmyv2_getTransactionByHash"
params = [ params = [ tx_hash ]
tx_hash
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_transaction_by_block_hash_and_index(block_hash, tx_index,
endpoint=_default_endpoint, timeout=_default_timeout def get_transaction_by_block_hash_and_index(
) -> dict: block_hash,
""" tx_index,
Get transaction based on index in list of transactions in a block by block hash endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get transaction based on index in list of transactions in a block by
block hash.
Parameters Parameters
---------- ----------
@ -262,21 +295,27 @@ def get_transaction_by_block_hash_and_index(block_hash, tx_index,
------------- -------------
https://api.hmny.io/#7c7e8d90-4984-4ebe-bb7e-d7adec167503 https://api.hmny.io/#7c7e8d90-4984-4ebe-bb7e-d7adec167503
""" """
method = 'hmyv2_getTransactionByBlockHashAndIndex' method = "hmyv2_getTransactionByBlockHashAndIndex"
params = [ params = [ block_hash, tx_index ]
block_hash,
tx_index
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_transaction_by_block_number_and_index(block_num, tx_index,
endpoint=_default_endpoint, timeout=_default_timeout def get_transaction_by_block_number_and_index(
) -> dict: block_num,
""" tx_index,
Get transaction based on index in list of transactions in a block by block number endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get transaction based on index in list of transactions in a block by
block number.
Parameters Parameters
---------- ----------
@ -302,19 +341,25 @@ def get_transaction_by_block_number_and_index(block_num, tx_index,
------------- -------------
https://api.hmny.io/#bcde8b1c-6ab9-4950-9835-3c7564e49c3e https://api.hmny.io/#bcde8b1c-6ab9-4950-9835-3c7564e49c3e
""" """
method = 'hmyv2_getTransactionByBlockNumberAndIndex' method = "hmyv2_getTransactionByBlockNumberAndIndex"
params = [ params = [ block_num, tx_index ]
block_num,
tx_index
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def get_transaction_receipt(tx_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> dict: timeout = timeout
""" )[ "result" ]
Get transaction receipt corresponding to tx_hash except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_transaction_receipt(
tx_hash,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get transaction receipt corresponding to tx_hash.
Parameters Parameters
---------- ----------
@ -335,7 +380,9 @@ def get_transaction_receipt(tx_hash, endpoint=_default_endpoint, timeout=_defaul
from: :obj:`str` Sender wallet address from: :obj:`str` Sender wallet address
gasUsed: :obj:`int` Gas used for the transaction gasUsed: :obj:`int` Gas used for the transaction
logs: :obj:`list` List of logs, each being a dict with keys as follows: logs: :obj:`list` List of logs, each being a dict with keys as follows:
address, blockHash, blockNumber, data, logIndex, removed, topics, transactionHash, transactionIndex address, blockHash, blockNumber
data, logIndex, removed
topics, transactionHash, transactionIndex
logsBloom :obj:`str` Bloom logs logsBloom :obj:`str` Bloom logs
shardID :obj:`int` Shard ID shardID :obj:`int` Shard ID
status :obj:`int` Status of transaction (0: pending, 1: success) status :obj:`int` Status of transaction (0: pending, 1: success)
@ -352,18 +399,25 @@ def get_transaction_receipt(tx_hash, endpoint=_default_endpoint, timeout=_defaul
------------- -------------
https://api.hmny.io/#0c2799f8-bcdc-41a4-b362-c3a6a763bb5e https://api.hmny.io/#0c2799f8-bcdc-41a4-b362-c3a6a763bb5e
""" """
method = 'hmyv2_getTransactionReceipt' method = "hmyv2_getTransactionReceipt"
params = [ params = [ tx_hash ]
tx_hash
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def send_raw_transaction(signed_tx, endpoint=_default_endpoint, timeout=_default_timeout) -> str: timeout = timeout
""" )[ "result" ]
Send signed transaction except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def send_raw_transaction(
signed_tx,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> str:
"""Send signed transaction.
Parameters Parameters
---------- ----------
@ -390,18 +444,25 @@ def send_raw_transaction(signed_tx, endpoint=_default_endpoint, timeout=_default
------------- -------------
https://api.hmny.io/#f40d124a-b897-4b7c-baf3-e0dedf8f40a0 https://api.hmny.io/#f40d124a-b897-4b7c-baf3-e0dedf8f40a0
""" """
params = [ params = [ signed_tx ]
signed_tx method = "hmyv2_sendRawTransaction"
]
method = 'hmyv2_sendRawTransaction'
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def send_and_confirm_raw_transaction(signed_tx, endpoint=_default_endpoint, timeout=_default_timeout) -> list: timeout = timeout
""" )[ "result" ]
Send signed transaction and wait for it to be confirmed except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def send_and_confirm_raw_transaction(
signed_tx,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Send signed transaction and wait for it to be confirmed.
Parameters Parameters
---------- ----------
@ -430,22 +491,29 @@ def send_and_confirm_raw_transaction(signed_tx, endpoint=_default_endpoint, time
------------- -------------
https://api.hmny.io/#f40d124a-b897-4b7c-baf3-e0dedf8f40a0 https://api.hmny.io/#f40d124a-b897-4b7c-baf3-e0dedf8f40a0
""" """
tx_hash = send_raw_transaction(signed_tx) tx_hash = send_raw_transaction( signed_tx, endpoint = endpoint )
start_time = time.time() start_time = time.time()
while((time.time() - start_time) <= timeout): while ( time.time() - start_time ) <= timeout:
tx_response = get_transaction_by_hash(tx_hash) tx_response = get_transaction_by_hash( tx_hash, endpoint = endpoint )
if tx_response is not None: if tx_response is not None:
if tx_response[ 'blockHash' ] != '0x0000000000000000000000000000000000000000000000000000000000000000': block_hash = tx_response.get( "blockHash", "0x00" )
unique_chars = "".join( set( list( block_hash[ 2 : ] ) ) )
if unique_chars != "0":
return tx_response return tx_response
time.sleep(random.uniform(0.2, 0.5)) time.sleep( random.uniform( 0.2, 0.5 ) )
raise TxConfirmationTimedoutError("Could not confirm transactions on-chain.") raise TxConfirmationTimedoutError(
"Could not confirm transaction on-chain."
)
############################### ###############################
# CrossShard Transaction RPCs # # CrossShard Transaction RPCs #
############################### ###############################
def get_pending_cx_receipts(endpoint=_default_endpoint, timeout=_default_timeout) -> list: def get_pending_cx_receipts(
""" endpoint = DEFAULT_ENDPOINT,
Get list of pending cross shard transactions timeout = DEFAULT_TIMEOUT
) -> list:
"""Get list of pending cross shard transactions.
Parameters Parameters
---------- ----------
@ -459,7 +527,7 @@ def get_pending_cx_receipts(endpoint=_default_endpoint, timeout=_default_timeout
list of CX receipts, each a dict with the following keys list of CX receipts, each a dict with the following keys
commitBitmap: :obj:`str` Hex represenation of aggregated signature bitmap commitBitmap: :obj:`str` Hex represenation of aggregated signature bitmap
commitSig: :obj:`str` Hex representation of aggregated signature commitSig: :obj:`str` Hex representation of aggregated signature
receipts: :obj:`list` list of dictionaries, each representing a cross shard transaction receipt receipts: :obj:`list` list of dictionaries, each a cross shard transaction receipt
amount: :obj:`int` Amount in ATTO amount: :obj:`int` Amount in ATTO
from: :obj:`str` From address from: :obj:`str` From address
to: :obj:`str` From address to: :obj:`str` From address
@ -490,15 +558,21 @@ def get_pending_cx_receipts(endpoint=_default_endpoint, timeout=_default_timeout
------------- -------------
https://api.hmny.io/#fe60070d-97b4-458d-9365-490b44c18851 https://api.hmny.io/#fe60070d-97b4-458d-9365-490b44c18851
""" """
method = 'hmyv2_getPendingCXReceipts' method = "hmyv2_getPendingCXReceipts"
try: try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] return rpc_request( method,
except KeyError as e: endpoint = endpoint,
raise InvalidRPCReplyError(method, endpoint) from e timeout = timeout )[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_cx_receipt_by_hash(cx_hash, endpoint = _default_endpoint, timeout = _default_timeout) -> dict:
""" def get_cx_receipt_by_hash(
Get cross shard receipt by hash on the receiving shard end point cx_hash,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get cross shard receipt by hash on the receiving shard end point.
Parameters Parameters
---------- ----------
@ -531,18 +605,26 @@ def get_cx_receipt_by_hash(cx_hash, endpoint = _default_endpoint, timeout = _def
------------- -------------
https://api.hmny.io/#3d6ad045-800d-4021-aeb5-30a0fbf724fe https://api.hmny.io/#3d6ad045-800d-4021-aeb5-30a0fbf724fe
""" """
params = [ params = [ cx_hash ]
cx_hash method = "hmyv2_getCXReceiptByHash"
]
method = 'hmyv2_getCXReceiptByHash'
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def resend_cx_receipt(cx_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> bool: timeout = timeout
""" )[ "result" ]
Resend the cross shard receipt to the receiving shard to re-process if the transaction did not pay out except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def resend_cx_receipt(
cx_hash,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> bool:
"""Resend the cross shard receipt to the receiving shard to re-process if
the transaction did not pay out.
Parameters Parameters
---------- ----------
@ -567,21 +649,28 @@ def resend_cx_receipt(cx_hash, endpoint=_default_endpoint, timeout=_default_time
------------- -------------
https://api.hmny.io/#c658b56b-d20b-480d-b71a-b0bc505d2164 https://api.hmny.io/#c658b56b-d20b-480d-b71a-b0bc505d2164
""" """
method = 'hmyv2_resendCx' method = "hmyv2_resendCx"
params = [ params = [ cx_hash ]
cx_hash
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
############################ ############################
# Staking Transaction RPCs # # Staking Transaction RPCs #
############################ ############################
def get_staking_transaction_by_hash(tx_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> dict: def get_staking_transaction_by_hash(
""" tx_hash,
Get staking transaction by hash endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get staking transaction by hash.
Parameters Parameters
---------- ----------
@ -619,20 +708,26 @@ def get_staking_transaction_by_hash(tx_hash, endpoint=_default_endpoint, timeout
------------- -------------
https://api.hmny.io/#296cb4d0-bce2-48e3-bab9-64c3734edd27 https://api.hmny.io/#296cb4d0-bce2-48e3-bab9-64c3734edd27
""" """
method = 'hmyv2_getStakingTransactionByHash' method = "hmyv2_getStakingTransactionByHash"
params = [ params = [ tx_hash ]
tx_hash
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_staking_transaction_by_block_hash_and_index(block_hash, tx_index,
endpoint=_default_endpoint, timeout=_default_timeout def get_staking_transaction_by_block_hash_and_index(
) -> dict: block_hash,
""" tx_index,
Get staking transaction by block hash and transaction index endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get staking transaction by block hash and transaction index.
Parameters Parameters
---------- ----------
@ -658,21 +753,26 @@ def get_staking_transaction_by_block_hash_and_index(block_hash, tx_index,
------------- -------------
https://api.hmny.io/#ba96cf61-61fe-464a-aa06-2803bb4b358f https://api.hmny.io/#ba96cf61-61fe-464a-aa06-2803bb4b358f
""" """
method = 'hmyv2_getStakingTransactionByBlockHashAndIndex' method = "hmyv2_getStakingTransactionByBlockHashAndIndex"
params = [ params = [ block_hash, tx_index ]
block_hash,
tx_index
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def get_staking_transaction_by_block_number_and_index(block_num, tx_index,
endpoint=_default_endpoint, timeout=_default_timeout def get_staking_transaction_by_block_number_and_index(
) -> dict: block_num,
""" tx_index,
Get staking transaction by block number and transaction index endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> dict:
"""Get staking transaction by block number and transaction index.
Parameters Parameters
---------- ----------
@ -698,19 +798,25 @@ def get_staking_transaction_by_block_number_and_index(block_num, tx_index,
------------- -------------
https://api.hmny.io/#fb41d717-1645-4d3e-8071-6ce8e1b65dd3 https://api.hmny.io/#fb41d717-1645-4d3e-8071-6ce8e1b65dd3
""" """
method = 'hmyv2_getStakingTransactionByBlockNumberAndIndex' method = "hmyv2_getStakingTransactionByBlockNumberAndIndex"
params = [ params = [ block_num, tx_index ]
block_num,
tx_index
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
def send_raw_staking_transaction(raw_tx, endpoint=_default_endpoint, timeout=_default_timeout) -> str: timeout = timeout
""" )[ "result" ]
Send signed staking transaction except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def send_raw_staking_transaction(
raw_tx,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> str:
"""Send signed staking transaction.
Parameters Parameters
---------- ----------
@ -737,11 +843,66 @@ def send_raw_staking_transaction(raw_tx, endpoint=_default_endpoint, timeout=_de
------------- -------------
https://api.hmny.io/#e8c17fe9-e730-4c38-95b3-6f1a5b1b9401 https://api.hmny.io/#e8c17fe9-e730-4c38-95b3-6f1a5b1b9401
""" """
method = 'hmyv2_sendRawStakingTransaction' method = "hmyv2_sendRawStakingTransaction"
params = [ params = [ raw_tx ]
raw_tx
]
try: try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] return rpc_request(
except KeyError as e: method,
raise InvalidRPCReplyError(method, endpoint) from e params = params,
endpoint = endpoint,
timeout = timeout
)[ "result" ]
except KeyError as exception:
raise InvalidRPCReplyError( method, endpoint ) from exception
def send_and_confirm_raw_staking_transaction(
signed_tx,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> list:
"""Send signed staking transaction and wait for it to be confirmed.
Parameters
----------
signed_tx: str
Hex representation of signed staking transaction
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
str
Transaction, see get_transaction_by_hash for structure
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint, or
RPCError
If transaction failed to be added to the pool
TxConfirmationTimedoutError
If transaction could not be confirmed within the timeout period
API Reference
-------------
https://api.hmny.io/#e8c17fe9-e730-4c38-95b3-6f1a5b1b9401
"""
tx_hash = send_raw_staking_transaction( signed_tx, endpoint = endpoint )
start_time = time.time()
while ( time.time() - start_time ) <= timeout:
tx_response = get_staking_transaction_by_hash(
tx_hash,
endpoint = endpoint
)
if tx_response is not None:
block_hash = tx_response.get( "blockHash", "0x00" )
unique_chars = "".join( set( list( block_hash[ 2 : ] ) ) )
if unique_chars != "0":
return tx_response
time.sleep( random.uniform( 0.2, 0.5 ) )
raise TxConfirmationTimedoutError(
"Could not confirm transaction on-chain."
)

@ -1,51 +1,50 @@
"""
Basic pyhmy utils like is_shard_active
ONE address format conversion
Chain id (str) to int conversion
"""
import json import json
import subprocess import subprocess
import os import os
import sys import sys
import datetime import datetime
import requests from eth_utils import to_checksum_address
from .blockchain import ( from .blockchain import get_latest_header
get_latest_header
)
from .rpc.exceptions import ( from .rpc.exceptions import ( RPCError, RequestsError, RequestsTimeoutError, )
RPCError,
RequestsError,
RequestsTimeoutError,
)
from .account import ( from .account import is_valid_address
is_valid_address
)
from .bech32.bech32 import ( from .bech32.bech32 import bech32_decode, bech32_encode, convertbits
bech32_decode,
convertbits
)
from eth_utils import to_checksum_address
datetime_format = "%Y-%m-%d %H:%M:%S.%f" class Typgpy( str ):
"""Typography constants for pretty printing.
class Typgpy(str): Note that an ENDC is needed to mark the end of a 'highlighted' text
segment.
""" """
Typography constants for pretty printing.
Note that an ENDC is needed to mark the end of a 'highlighted' text segment. HEADER = "\033[95m"
OKBLUE = "\033[94m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
BOLD = "\033[1m"
UNDERLINE = "\033[4m"
def chain_id_to_int( chain_id ):
""" """
HEADER = '\033[95m' If chain_id is a string, converts it to int.
OKBLUE = '\033[94m' If chain_id is an int, returns the int.
OKGREEN = '\033[92m'
WARNING = '\033[93m' Else raises TypeError
FAIL = '\033[91m' """
ENDC = '\033[0m' chain_ids = dict(
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def chain_id_to_int(chainId):
chainIds = dict(
Default = 0, Default = 0,
EthMainnet = 1, EthMainnet = 1,
Morden = 2, Morden = 2,
@ -63,41 +62,56 @@ def chain_id_to_int(chainId):
HmyLocal = 2, HmyLocal = 2,
HmyPangaea = 3, HmyPangaea = 3,
) )
if isinstance(chainId, str):
assert chainId in chainIds, f'ChainId {chainId} is not valid' # do not validate integer chainids, only known strings
return chainIds.get(chainId) if isinstance( chain_id, str ):
elif isinstance(chainId, int): assert (
assert chainId in chainIds.values(), f'Unknown chain id {chainId}' chain_id in chain_ids
return chainId ), f"Chain {chain_id} unknown, specify an integer chainId"
else: return chain_ids.get( chain_id )
raise TypeError( 'chainId must be str or int' ) if isinstance( chain_id, int ):
return chain_id
raise TypeError( "chainId must be str or int" )
def get_gopath(): def get_gopath():
""" """
:returns The go-path, assuming that go is installed. :returns The go-path, assuming that go is installed.
""" """
return subprocess.check_output(["go", "env", "GOPATH"]).decode().strip() return subprocess.check_output( [ "go", "env", "GOPATH" ] ).decode().strip()
def get_goversion(): def get_goversion():
""" """
:returns The go-version, assuming that go is installed. :returns The go-version, assuming that go is installed.
""" """
return subprocess.check_output(["go", "version"]).decode().strip() return subprocess.check_output( [ "go", "version" ] ).decode().strip()
def convert_one_to_hex(addr):
""" def convert_one_to_hex( addr ):
Given a one address, convert it to hex checksum address """Given a one address, convert it to hex checksum address."""
""" if not is_valid_address( addr ):
if not is_valid_address(addr): return to_checksum_address( addr )
return to_checksum_address(addr) _, data = bech32_decode( addr )
hrp, data = bech32_decode(addr) buf = convertbits( data, 5, 8, False )
buf = convertbits(data, 5, 8, False) address = "0x" + "".join( f"{x:02x}" for x in buf )
address = '0x' + ''.join('{:02x}'.format(x) for x in buf) return str( to_checksum_address( address ) )
return to_checksum_address(address)
def convert_hex_to_one( addr ):
"""Given a hex address, convert it to a one address."""
if is_valid_address( addr ):
return addr
checksum_addr = str( to_checksum_address( addr ) )
data = bytearray.fromhex(
checksum_addr[ 2 : ] if checksum_addr
.startswith( "0x" ) else checksum_addr
)
buf = convertbits( data, 8, 5 )
return str( bech32_encode( "one", buf ) )
def is_active_shard(endpoint, delay_tolerance=60): def is_active_shard( endpoint, delay_tolerance = 60 ):
""" """
:param endpoint: The endpoint of the SHARD to check :param endpoint: The endpoint of the SHARD to check
:param delay_tolerance: The time (in seconds) that the shard timestamp can be behind :param delay_tolerance: The time (in seconds) that the shard timestamp can be behind
@ -105,12 +119,15 @@ def is_active_shard(endpoint, delay_tolerance=60):
""" """
try: try:
curr_time = datetime.datetime.utcnow() curr_time = datetime.datetime.utcnow()
latest_header = get_latest_header(endpoint=endpoint) latest_header = get_latest_header( endpoint = endpoint )
time_str = latest_header["timestamp"][:19] + '.0' # Fit time format time_str = latest_header[ "timestamp" ][ : 19 ] + ".0" # Fit time format
timestamp = datetime.datetime.strptime(time_str, "%Y-%m-%d %H:%M:%S.%f").replace(tzinfo=None) timestamp = datetime.datetime.strptime(
time_str,
"%Y-%m-%d %H:%M:%S.%f"
).replace( tzinfo = None )
time_delta = curr_time - timestamp time_delta = curr_time - timestamp
return abs(time_delta.seconds) < delay_tolerance return abs( time_delta.seconds ) < delay_tolerance
except (RPCError, RequestsError, RequestsTimeoutError): except ( RPCError, RequestsError, RequestsTimeoutError ):
return False return False
@ -125,27 +142,37 @@ def get_bls_build_variables():
""" """
variables = {} variables = {}
try: try:
openssl_dir = subprocess.check_output(["which", "openssl"]).decode().strip().split("\n")[0] openssl_dir = (
except (IndexError, subprocess.CalledProcessError) as e: subprocess.check_output(
raise RuntimeError("`openssl` not found") from e [ "which",
"openssl" ]
).decode().strip().split( "\n",
maxsplit = 1 )[ 0 ]
)
except ( IndexError, subprocess.CalledProcessError ) as exception:
raise RuntimeError( "`openssl` not found" ) from exception
hmy_path = f"{get_gopath()}/src/github.com/harmony-one" hmy_path = f"{get_gopath()}/src/github.com/harmony-one"
bls_dir = f"{hmy_path}/bls" bls_dir = f"{hmy_path}/bls"
mcl_dir = f"{hmy_path}/mcl" mcl_dir = f"{hmy_path}/mcl"
assert os.path.exists(bls_dir), f"Harmony BLS repo not found at {bls_dir}" assert os.path.exists( bls_dir ), f"Harmony BLS repo not found at {bls_dir}"
assert os.path.exists(mcl_dir), f"Harmony MCL repo not found at {mcl_dir}" assert os.path.exists( mcl_dir ), f"Harmony MCL repo not found at {mcl_dir}"
if sys.platform.startswith("darwin"): if sys.platform.startswith( "darwin" ):
variables["CGO_CFLAGS"] = f"-I{bls_dir}/include -I{mcl_dir}/include -I{openssl_dir}/include" variables[
variables["CGO_LDFLAGS"] = f"-L{bls_dir}/lib -L{openssl_dir}/lib" "CGO_CFLAGS"
variables["LD_LIBRARY_PATH"] = f"{bls_dir}/lib:{mcl_dir}/lib:{openssl_dir}/lib" ] = f"-I{bls_dir}/include -I{mcl_dir}/include -I{openssl_dir}/include"
variables["DYLD_FALLBACK_LIBRARY_PATH"] = variables["LD_LIBRARY_PATH"] variables[ "CGO_LDFLAGS" ] = f"-L{bls_dir}/lib -L{openssl_dir}/lib"
variables[ "LD_LIBRARY_PATH"
] = f"{bls_dir}/lib:{mcl_dir}/lib:{openssl_dir}/lib"
variables[ "DYLD_FALLBACK_LIBRARY_PATH" ] = variables[ "LD_LIBRARY_PATH"
]
else: else:
variables["CGO_CFLAGS"] = f"-I{bls_dir}/include -I{mcl_dir}/include" variables[ "CGO_CFLAGS" ] = f"-I{bls_dir}/include -I{mcl_dir}/include"
variables["CGO_LDFLAGS"] = f"-L{bls_dir}/lib" variables[ "CGO_LDFLAGS" ] = f"-L{bls_dir}/lib"
variables["LD_LIBRARY_PATH"] = f"{bls_dir}/lib:{mcl_dir}/lib" variables[ "LD_LIBRARY_PATH" ] = f"{bls_dir}/lib:{mcl_dir}/lib"
return variables return variables
def json_load(string, **kwargs): def json_load( string, **kwargs ):
""" """
:param string: The JSON string to load :param string: The JSON string to load
:returns A dictionary loaded from a JSON string to a dictionary. :returns A dictionary loaded from a JSON string to a dictionary.
@ -154,7 +181,7 @@ def json_load(string, **kwargs):
Note that this prints the failed input should an error arise. Note that this prints the failed input should an error arise.
""" """
try: try:
return json.loads(string, **kwargs) return json.loads( string, **kwargs )
except Exception as e: except Exception as exception:
print(f"{Typgpy.FAIL}Could not parse input: '{string}'{Typgpy.ENDC}") print( f"{Typgpy.FAIL}Could not parse input: '{string}'{Typgpy.ENDC}" )
raise e from e raise exception

@ -1,63 +1,54 @@
"""
Load validator information from Harmony blockchain
Create and edit validators
"""
import json import json
from decimal import Decimal, InvalidOperation
from eth_account.datastructures import ( from eth_account.datastructures import SignedTransaction
SignedTransaction
)
from decimal import (
Decimal,
InvalidOperation
)
from .account import (
get_balance,
is_valid_address
)
from .numbers import ( from .account import is_valid_address
convert_one_to_atto
)
from .exceptions import ( from .constants import (
InvalidValidatorError, DEFAULT_ENDPOINT,
RPCError, DEFAULT_TIMEOUT,
RequestsError, NAME_CHAR_LIMIT,
RequestsTimeoutError IDENTITY_CHAR_LIMIT,
WEBSITE_CHAR_LIMIT,
SECURITY_CONTACT_CHAR_LIMIT,
DETAILS_CHAR_LIMIT,
MIN_REQUIRED_DELEGATION,
) )
from .staking import ( from .exceptions import InvalidValidatorError
get_all_validator_addresses,
get_validator_information
)
from .staking_structures import ( from .rpc.exceptions import ( RPCError, RequestsError, RequestsTimeoutError, )
Directive
)
from .staking_signing import ( from .staking import get_all_validator_addresses, get_validator_information
sign_staking_transaction
)
_default_endpoint = 'http://localhost:9500' from .staking_structures import Directive
_default_timeout = 30
# TODO: Add unit testing from .staking_signing import sign_staking_transaction
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 = convert_one_to_atto(10000) # in ATTO
def __init__(self, address): class Validator: # pylint: disable=too-many-instance-attributes, too-many-public-methods
if not isinstance(address, str): """
raise InvalidValidatorError(1, 'given ONE address was not a string') Harmony validator
if not is_valid_address(address): """
raise InvalidValidatorError(1, f'{address} is not valid ONE address') def __init__( self, address ):
if not isinstance( address, str ):
raise InvalidValidatorError(
1,
"given ONE address was not a string"
)
if not is_valid_address( address ):
raise InvalidValidatorError(
1,
f"{address} is not valid ONE address"
)
self._address = address self._address = address
self._bls_keys = [] self._bls_keys = []
self._bls_key_sigs = []
self._name = None self._name = None
self._identity = None self._identity = None
@ -73,35 +64,35 @@ class Validator:
self._max_change_rate = None self._max_change_rate = None
self._max_rate = None self._max_rate = None
def _sanitize_input(self, data, check_str=False) -> str: def _sanitize_input( self, data, check_str = False ) -> str:
""" """If data is None, return '' else return data.
If data is None, return '' else return data
Raises Raises
------ ------
InvalidValidatorError if check_str is True and str is not passed InvalidValidatorError if check_str is True and str is not passed
""" """
if check_str: if check_str:
if not isinstance(data, str): if not isinstance( data, str ):
raise InvalidValidatorError(3, f'Expected data to be string to avoid floating point precision issues but got {data}') raise InvalidValidatorError(
return '' if not data else str(data) 3,
"Expected data to be string "
def __str__(self) -> str: f"to avoid floating point precision issues but got {data}",
""" )
Returns JSON string representation of Validator fields return "" if not data else str( data )
"""
def __str__( self ) -> str:
"""Returns JSON string representation of Validator fields."""
info = self.export() info = self.export()
for key, value in info.items(): for key, value in info.items():
if isinstance(value, Decimal): if isinstance( value, Decimal ):
info[key] = str(value) info[ key ] = str( value )
return json.dumps(info) return json.dumps( info )
def __repr__(self) -> str: def __repr__( self ) -> str:
return f'<Validator: {hex(id(self))}>' return f"<Validator: {hex(id(self))}>"
def get_address(self) -> str: def get_address( self ) -> str:
""" """Get validator address.
Get validator address
Returns Returns
------- -------
@ -110,39 +101,36 @@ class Validator:
""" """
return self._address return self._address
def add_bls_key(self, key) -> bool: def add_bls_key( self, key ) -> bool:
""" """Add BLS public key to validator BLS keys if not already in list.
Add BLS public key to validator BLS keys if not already in list
Returns Returns
------- -------
bool bool
If adding BLS key succeeded If adding BLS key succeeded
""" """
key = self._sanitize_input(key) key = self._sanitize_input( key )
if key not in self._bls_keys: if key not in self._bls_keys:
self._bls_keys.append(key) self._bls_keys.append( key )
return True return True
return False return False
def remove_bls_key(self, key) -> bool: def remove_bls_key( self, key ) -> bool:
""" """Remove BLS public key from validator BLS keys if exists.
Remove BLS public key from validator BLS keys if exists
Returns Returns
------- -------
bool bool
If removing BLS key succeeded If removing BLS key succeeded
""" """
key = self._sanitize_input(key) key = self._sanitize_input( key )
if key in self._bls_keys: if key in self._bls_keys:
self._bls_keys.remove(key) self._bls_keys.remove( key )
return True return True
return False return False
def get_bls_keys(self) -> list: def get_bls_keys( self ) -> list:
""" """Get list of validator BLS keys.
Get list of validator BLS keys
Returns Returns
------- -------
@ -151,9 +139,46 @@ class Validator:
""" """
return self._bls_keys return self._bls_keys
def set_name(self, name): def add_bls_key_sig( self, key ) -> bool:
"""Add BLS public key to validator BLS keys if not already in list.
Returns
-------
bool
If adding BLS key succeeded
""" """
Set validator name key = self._sanitize_input( key )
if key not in self._bls_key_sigs:
self._bls_key_sigs.append( key )
return True
return False
def remove_bls_key_sig( self, key ) -> bool:
"""Remove BLS public key from validator BLS keys if exists.
Returns
-------
bool
If removing BLS key succeeded
"""
key = self._sanitize_input( key )
if key in self._bls_key_sigs:
self._bls_key_sigs.remove( key )
return True
return False
def get_bls_key_sigs( self ) -> list:
"""Get list of validator BLS keys.
Returns
-------
list
List of validator BLS keys (strings)
"""
return self._bls_key_sigs
def set_name( self, name ):
"""Set validator name.
Parameters Parameters
---------- ----------
@ -165,14 +190,16 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
name = self._sanitize_input(name) name = self._sanitize_input( name )
if len(name) > self.name_char_limit: if len( name ) > NAME_CHAR_LIMIT:
raise InvalidValidatorError(3, f'Name must be less than {self.name_char_limit} characters') raise InvalidValidatorError(
3,
f"Name must be less than {NAME_CHAR_LIMIT} characters"
)
self._name = name self._name = name
def get_name(self) -> str: def get_name( self ) -> str:
""" """Get validator name.
Get validator name
Returns Returns
------- -------
@ -181,9 +208,8 @@ class Validator:
""" """
return self._name return self._name
def set_identity(self, identity): def set_identity( self, identity ):
""" """Set validator identity.
Set validator identity
Parameters Parameters
---------- ----------
@ -195,14 +221,16 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
identity = self._sanitize_input(identity) identity = self._sanitize_input( identity )
if len(identity) > self.identity_char_limit: if len( identity ) > IDENTITY_CHAR_LIMIT:
raise InvalidValidatorError(3, f'Identity must be less than {self.identity_char_limit} characters') raise InvalidValidatorError(
3,
f"Identity must be less than {IDENTITY_CHAR_LIMIT} characters"
)
self._identity = identity self._identity = identity
def get_identity(self) -> str: def get_identity( self ) -> str:
""" """Get validator identity.
Get validator identity
Returns Returns
------- -------
@ -211,9 +239,8 @@ class Validator:
""" """
return self._identity return self._identity
def set_website(self, website): def set_website( self, website ):
""" """Set validator website.
Set validator website
Parameters Parameters
---------- ----------
@ -225,14 +252,16 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
website = self._sanitize_input(website) website = self._sanitize_input( website )
if len(website) > self.website_char_limit: if len( website ) > WEBSITE_CHAR_LIMIT:
raise InvalidValidatorError(3, f'Website must be less than {self.website_char_limit} characters') raise InvalidValidatorError(
3,
f"Website must be less than {WEBSITE_CHAR_LIMIT} characters"
)
self._website = website self._website = website
def get_website(self) -> str: def get_website( self ) -> str:
""" """Get validator website.
Get validator website
Returns Returns
------- -------
@ -241,9 +270,8 @@ class Validator:
""" """
return self._website return self._website
def set_security_contact(self, contact): def set_security_contact( self, contact ):
""" """Set validator security contact.
Set validator security contact
Parameters Parameters
---------- ----------
@ -255,14 +283,16 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
contact = self._sanitize_input(contact) contact = self._sanitize_input( contact )
if len(contact) > self.security_contact_char_limit: if len( contact ) > SECURITY_CONTACT_CHAR_LIMIT:
raise InvalidValidatorError(3, f'Security contact must be less than {self.security_contact_char_limit} characters') raise InvalidValidatorError(
3,
f"Security contact must be less than {SECURITY_CONTACT_CHAR_LIMIT} characters",
)
self._security_contact = contact self._security_contact = contact
def get_security_contact(self) -> str: def get_security_contact( self ) -> str:
""" """Get validator security contact.
Get validator security contact
Returns Returns
------- -------
@ -271,9 +301,8 @@ class Validator:
""" """
return self._security_contact return self._security_contact
def set_details(self, details): def set_details( self, details ):
""" """Set validator details.
Set validator details
Parameters Parameters
---------- ----------
@ -285,14 +314,16 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
details = self._sanitize_input(details) details = self._sanitize_input( details )
if len(details) > self.details_char_limit: if len( details ) > DETAILS_CHAR_LIMIT:
raise InvalidValidatorError(3, f'Details must be less than {self.details_char_limit} characters') raise InvalidValidatorError(
3,
f"Details must be less than {DETAILS_CHAR_LIMIT} characters"
)
self._details = details self._details = details
def get_details(self) -> str: def get_details( self ) -> str:
""" """Get validator details.
Get validator details
Returns Returns
------- -------
@ -301,9 +332,8 @@ class Validator:
""" """
return self._details return self._details
def set_min_self_delegation(self, delegation): def set_min_self_delegation( self, delegation ):
""" """Set validator min self delegation.
Set validator min self delegation
Parameters Parameters
---------- ----------
@ -315,18 +345,23 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
delegation = self._sanitize_input(delegation) delegation = self._sanitize_input( delegation )
try: try:
delegation = Decimal(delegation) delegation = Decimal( delegation )
except (TypeError, InvalidOperation) as e: except ( TypeError, InvalidOperation ) as exception:
raise InvalidValidatorError(3, 'Min self delegation must be a number') from e raise InvalidValidatorError(
if delegation < self.min_required_delegation: 3,
raise InvalidValidatorError(3, f'Min self delegation must be greater than {self.min_required_delegation} ATTO') "Min self delegation must be a number"
) from exception
if delegation < MIN_REQUIRED_DELEGATION:
raise InvalidValidatorError(
3,
f"Min self delegation must be greater than {MIN_REQUIRED_DELEGATION} ATTO",
)
self._min_self_delegation = delegation self._min_self_delegation = delegation
def get_min_self_delegation(self) -> Decimal: def get_min_self_delegation( self ) -> Decimal:
""" """Get validator min self delegation.
Get validator min self delegation
Returns Returns
------- -------
@ -335,9 +370,8 @@ class Validator:
""" """
return self._min_self_delegation return self._min_self_delegation
def set_max_total_delegation(self, max_delegation): def set_max_total_delegation( self, max_delegation ):
""" """Set validator max total delegation.
Set validator max total delegation
Parameters Parameters
---------- ----------
@ -349,22 +383,30 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
max_delegation = self._sanitize_input(max_delegation) max_delegation = self._sanitize_input( max_delegation )
try: try:
max_delegation = Decimal(max_delegation) max_delegation = Decimal( max_delegation )
except (TypeError, InvalidOperation) as e: except ( TypeError, InvalidOperation ) as exception:
raise InvalidValidatorError(3, 'Max total delegation must be a number') from e raise InvalidValidatorError(
3,
"Max total delegation must be a number"
) from exception
if self._min_self_delegation: if self._min_self_delegation:
if max_delegation < self._min_self_delegation: if max_delegation < self._min_self_delegation:
raise InvalidValidatorError(3, f'Max total delegation must be greater than min self delegation: ' raise InvalidValidatorError(
'{self._min_self_delegation}') 3,
"Max total delegation must be greater than min self delegation: "
f"{self._min_self_delegation}",
)
else: else:
raise InvalidValidatorError(4, 'Min self delegation must be set before max total delegation') raise InvalidValidatorError(
4,
"Min self delegation must be set before max total delegation"
)
self._max_total_delegation = max_delegation self._max_total_delegation = max_delegation
def get_max_total_delegation(self) -> Decimal: def get_max_total_delegation( self ) -> Decimal:
""" """Get validator max total delegation.
Get validator max total delegation
Returns Returns
------- -------
@ -373,9 +415,8 @@ class Validator:
""" """
return self._max_total_delegation return self._max_total_delegation
def set_amount(self, amount): def set_amount( self, amount ):
""" """Set validator initial delegation amount.
Set validator initial delegation amount
Parameters Parameters
---------- ----------
@ -387,28 +428,42 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
amount = self._sanitize_input(amount) amount = self._sanitize_input( amount )
try: try:
amount = Decimal(amount) amount = Decimal( amount )
except (TypeError, InvalidOperation) as e: except ( TypeError, InvalidOperation ) as exception:
raise InvalidValidatorError(3, 'Amount must be a number') from e raise InvalidValidatorError(
3,
"Amount must be a number"
) from exception
if self._min_self_delegation: if self._min_self_delegation:
if amount < self._min_self_delegation: if amount < self._min_self_delegation:
raise InvalidValidatorError(3, 'Amount must be greater than min self delegation: ' raise InvalidValidatorError(
f'{self._min_self_delegation}') 3,
"Amount must be greater than min self delegation: "
f"{self._min_self_delegation}",
)
else: else:
raise InvalidValidatorError(4, 'Min self delegation must be set before amount') raise InvalidValidatorError(
4,
"Min self delegation must be set before amount"
)
if self._max_total_delegation: if self._max_total_delegation:
if amount > self._max_total_delegation: if amount > self._max_total_delegation:
raise InvalidValidatorError(3, 'Amount must be less than max total delegation: ' raise InvalidValidatorError(
f'{self._max_total_delegation}') 3,
"Amount must be less than max total delegation: "
f"{self._max_total_delegation}",
)
else: else:
raise InvalidValidatorError(4, 'Max total delegation must be set before amount') raise InvalidValidatorError(
4,
"Max total delegation must be set before amount"
)
self._inital_delegation = amount self._inital_delegation = amount
def get_amount(self) -> Decimal: def get_amount( self ) -> Decimal:
""" """Get validator initial delegation amount.
Get validator initial delegation amount
Returns Returns
------- -------
@ -417,9 +472,8 @@ class Validator:
""" """
return self._inital_delegation return self._inital_delegation
def set_max_rate(self, rate): def set_max_rate( self, rate ):
""" """Set validator max commission rate.
Set validator max commission rate
Parameters Parameters
---------- ----------
@ -431,18 +485,20 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
rate = self._sanitize_input(rate, True) rate = self._sanitize_input( rate, True )
try: try:
rate = Decimal(rate) rate = Decimal( rate )
except (TypeError, InvalidOperation) as e: except ( TypeError, InvalidOperation ) as exception:
raise InvalidValidatorError(3, 'Max rate must be a number') from e raise InvalidValidatorError(
3,
"Max rate must be a number"
) from exception
if rate < 0 or rate > 1: if rate < 0 or rate > 1:
raise InvalidValidatorError(3, 'Max rate must be between 0 and 1') raise InvalidValidatorError( 3, "Max rate must be between 0 and 1" )
self._max_rate = rate self._max_rate = rate
def get_max_rate(self) -> Decimal: def get_max_rate( self ) -> Decimal:
""" """Get validator max commission rate.
Get validator max commission rate
Returns Returns
------- -------
@ -451,9 +507,8 @@ class Validator:
""" """
return self._max_rate return self._max_rate
def set_max_change_rate(self, rate): def set_max_change_rate( self, rate ):
""" """Set validator max commission change rate.
Set validator max commission change rate
Parameters Parameters
---------- ----------
@ -465,23 +520,34 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
rate = self._sanitize_input(rate, True) rate = self._sanitize_input( rate, True )
try: try:
rate = Decimal(rate) rate = Decimal( rate )
except (TypeError, InvalidOperation) as e: except ( TypeError, InvalidOperation ) as exception:
raise InvalidValidatorError(3, 'Max change rate must be a number') from e raise InvalidValidatorError(
3,
"Max change rate must be a number"
) from exception
if rate < 0: if rate < 0:
raise InvalidValidatorError(3, 'Max change rate must be greater than or equal to 0') raise InvalidValidatorError(
3,
"Max change rate must be greater than or equal to 0"
)
if self._max_rate: if self._max_rate:
if rate > 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}') raise InvalidValidatorError(
3,
f"Max change rate must be less than or equal to max rate: {self._max_rate}",
)
else: else:
raise InvalidValidatorError(4, 'Max rate must be set before max change rate') raise InvalidValidatorError(
4,
"Max rate must be set before max change rate"
)
self._max_change_rate = rate self._max_change_rate = rate
def get_max_change_rate(self) -> Decimal: def get_max_change_rate( self ) -> Decimal:
""" """Get validator max commission change rate.
Get validator max commission change rate
Returns Returns
------- -------
@ -490,9 +556,8 @@ class Validator:
""" """
return self._max_change_rate return self._max_change_rate
def set_rate(self, rate): def set_rate( self, rate ):
""" """Set validator commission rate.
Set validator commission rate
Parameters Parameters
---------- ----------
@ -504,23 +569,31 @@ class Validator:
InvalidValidatorError InvalidValidatorError
If input is invalid If input is invalid
""" """
rate = self._sanitize_input(rate, True) rate = self._sanitize_input( rate, True )
try: try:
rate = Decimal(rate) rate = Decimal( rate )
except (TypeError, InvalidOperation) as e: except ( TypeError, InvalidOperation ) as exception:
raise InvalidValidatorError(3, 'Rate must be a number') from e raise InvalidValidatorError(
3,
"Rate must be a number"
) from exception
if rate < 0: if rate < 0:
raise InvalidValidatorError(3, 'Rate must be greater than or equal to 0') raise InvalidValidatorError(
3,
"Rate must be greater than or equal to 0"
)
if self._max_rate: if self._max_rate:
if rate > 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}') raise InvalidValidatorError(
3,
f"Rate must be less than or equal to max rate: {self._max_rate}"
)
else: else:
raise InvalidValidatorError(4, 'Max rate must be set before rate') raise InvalidValidatorError( 4, "Max rate must be set before rate" )
self._rate = rate self._rate = rate
def get_rate(self) -> Decimal: def get_rate( self ) -> Decimal:
""" """Get validator commission rate.
Get validator commission rate
Returns Returns
------- -------
@ -529,9 +602,12 @@ class Validator:
""" """
return self._rate return self._rate
def does_validator_exist(self, endpoint=_default_endpoint, timeout=_default_timeout) -> bool: def does_validator_exist(
""" self,
Check if validator exists on blockchain endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
) -> bool:
"""Check if validator exists on blockchain.
Parameters Parameters
---------- ----------
@ -550,14 +626,13 @@ class Validator:
RPCError, RequestsError, RequestsTimeoutError RPCError, RequestsError, RequestsTimeoutError
If unable to get list of validators on chain If unable to get list of validators on chain
""" """
all_validators = get_all_validator_addresses(endpoint, timeout) all_validators = get_all_validator_addresses( endpoint, timeout )
if self._address in all_validators: if self._address in all_validators:
return True return True
return False return False
def load(self, info): def load( self, info ):
""" """Import validator information.
Import validator information
Parameters Parameters
---------- ----------
@ -578,6 +653,7 @@ class Validator:
"max-rate": '0', "max-rate": '0',
"max-change-rate": '0', "max-change-rate": '0',
"bls-public-keys": [ "" ] "bls-public-keys": [ "" ]
"bls-key-sigs": [ "" ]
} }
Raises Raises
@ -586,29 +662,41 @@ class Validator:
If input value is invalid If input value is invalid
""" """
try: try:
self.set_name(info['name']) self.set_name( info[ "name" ] )
self.set_identity(info['identity']) self.set_identity( info[ "identity" ] )
self.set_website(info['website']) self.set_website( info[ "website" ] )
self.set_details(info['details']) self.set_details( info[ "details" ] )
self.set_security_contact(info['security-contact']) self.set_security_contact( info[ "security-contact" ] )
self.set_min_self_delegation(info['min-self-delegation']) self.set_min_self_delegation( info[ "min-self-delegation" ] )
self.set_max_total_delegation(info['max-total-delegation']) self.set_max_total_delegation( info[ "max-total-delegation" ] )
self.set_amount(info['amount']) self.set_amount( info[ "amount" ] )
self.set_max_rate(info['max-rate']) self.set_max_rate( info[ "max-rate" ] )
self.set_max_change_rate(info['max-change-rate']) self.set_max_change_rate( info[ "max-change-rate" ] )
self.set_rate(info['rate']) self.set_rate( info[ "rate" ] )
self._bls_keys = [] self._bls_keys = []
for key in info['bls-public-keys']: for key in info[ "bls-public-keys" ]:
self.add_bls_key(key) self.add_bls_key( key )
except KeyError as e:
raise InvalidValidatorError(3, 'Info has missing key') from e self._bls_key_sigs = []
for key in info[ "bls-key-sigs" ]:
def load_from_blockchain(self, endpoint=_default_endpoint, timeout=_default_timeout): self.add_bls_key_sig( key )
""" except KeyError as exception:
Import validator information from blockchain with given address raise InvalidValidatorError(
3,
"Info has missing key"
) from exception
def load_from_blockchain(
self,
endpoint = DEFAULT_ENDPOINT,
timeout = DEFAULT_TIMEOUT
):
"""Import validator information from blockchain with given address At
the moment, this is unable to fetch the BLS Signature, which is not
implemented in the Node API.
Parameters Parameters
---------- ----------
@ -623,38 +711,55 @@ class Validator:
If any error occur getting & importing validator information from the blockchain If any error occur getting & importing validator information from the blockchain
""" """
try: try:
if not self.does_validator_exist(endpoint, timeout): if not self.does_validator_exist( endpoint, timeout ):
raise InvalidValidatorError(5, f'Validator does not exist on chain according to {endpoint}') raise InvalidValidatorError(
except (RPCError, RequestsError, RequestsTimeoutError) as e: 5,
raise InvalidValidatorError(5, 'Error requesting validator information') from e f"Validator does not exist on chain according to {endpoint}"
)
except ( RPCError, RequestsError, RequestsTimeoutError ) as exception:
raise InvalidValidatorError(
5,
"Error requesting validator information"
) from exception
try: try:
validator_info = get_validator_information(self._address, endpoint, timeout) validator_info = get_validator_information(
except (RPCError, RequestsError, RequestsTimeoutError) as e: self._address,
raise InvalidValidatorError(5, 'Error requesting validator information') from e endpoint,
timeout
)
except ( RPCError, RequestsError, RequestsTimeoutError ) as exception:
raise InvalidValidatorError(
5,
"Error requesting validator information"
) from exception
# Skip additional sanity checks when importing from chain # Skip additional sanity checks when importing from chain
try: try:
info = validator_info['validator'] info = validator_info[ "validator" ]
self._name = info['name'] self._name = info[ "name" ]
self._identity = info['identity'] self._identity = info[ "identity" ]
self._website = info['website'] self._website = info[ "website" ]
self._details = info['details'] self._details = info[ "details" ]
self._security_contact = info['security-contact'] self._security_contact = info[ "security-contact" ]
self._min_self_delegation = info['min-self-delegation'] self._min_self_delegation = info[ "min-self-delegation" ]
self._max_total_delegation = info['max-total-delegation'] self._max_total_delegation = info[ "max-total-delegation" ]
self._inital_delegation = self._min_self_delegation # Since validator exists, set initial delegation to 0 self._inital_delegation = (
self._min_self_delegation
self._max_rate = Decimal(info['max-rate']) ) # Since validator exists, set initial delegation to 0
self._max_change_rate = Decimal(info['max-change-rate'])
self._rate = Decimal(info['rate']) self._max_rate = Decimal( info[ "max-rate" ] )
self._bls_keys = info[ 'bls-public-keys' ] self._max_change_rate = Decimal( info[ "max-change-rate" ] )
except KeyError as e: self._rate = Decimal( info[ "rate" ] )
raise InvalidValidatorError(5, 'Error importing validator information from RPC result') from e self._bls_keys = info[ "bls-public-keys" ]
except KeyError as exception:
def export(self) -> dict: raise InvalidValidatorError(
""" 5,
Export validator information as dict "Error importing validator information from RPC result"
) from exception
def export( self ) -> dict:
"""Export validator information as dict.
Returns Returns
------- -------
@ -674,13 +779,16 @@ class Validator:
"rate": self._rate, "rate": self._rate,
"max-rate": self._max_rate, "max-rate": self._max_rate,
"max-change-rate": self._max_change_rate, "max-change-rate": self._max_change_rate,
"bls-public-keys": self._bls_keys "bls-public-keys": self._bls_keys,
"bls-key-sigs": self._bls_key_sigs,
} }
return info return info
def sign_create_validator_transaction(self, nonce, gas_price, gas_limit, private_key, chain_id=None) -> SignedTransaction: def sign_create_validator_transaction( # pylint: disable=too-many-arguments
""" self, nonce, gas_price, gas_limit, private_key, chain_id=None
Create but not post a transaction to Create the Validator using private_key ) -> SignedTransaction:
"""Create but not post a transaction to Create the Validator using
private_key.
Returns Returns
------- -------
@ -696,18 +804,31 @@ class Validator:
https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L413 https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L413
""" """
info = self.export().copy() info = self.export().copy()
info['directive'] = Directive.CreateValidator info[ "directive" ] = Directive.CreateValidator
info['validatorAddress'] = info.pop('validator-addr') # change the key info[ "validatorAddress" ] = info.pop(
info['nonce'] = nonce "validator-addr"
info['gasPrice'] = gas_price ) # change the key
info['gasLimit'] = gas_limit info[ "nonce" ] = nonce
info[ "gasPrice" ] = gas_price
info[ "gasLimit" ] = gas_limit
if chain_id: if chain_id:
info['chainId'] = chain_id info[ "chainId" ] = chain_id
return sign_staking_transaction(info, private_key) return sign_staking_transaction( info, private_key )
def sign_edit_validator_transaction(self, nonce, gas_price, gas_limit, rate, bls_key_to_add, bls_key_to_remove, private_key, chain_id=None) -> SignedTransaction: def sign_edit_validator_transaction( # pylint: disable=too-many-arguments
""" self,
Create but not post a transaction to Edit the Validator using private_key nonce,
gas_price,
gas_limit,
rate,
bls_key_to_remove,
bls_key_to_add,
bls_key_to_add_sig,
private_key,
chain_id=None,
) -> SignedTransaction:
"""Create but not post a transaction to Edit the Validator using
private_key.
Returns Returns
------- -------
@ -722,21 +843,24 @@ class Validator:
------------- -------------
https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L460 https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L460
""" """
self.set_rate(rate) self.set_rate( rate )
self.add_bls_key(bls_key_to_add) self.add_bls_key( bls_key_to_add )
self.remove_bls_key(bls_key_to_remove) self.remove_bls_key( bls_key_to_remove )
info = self.export().copy() info = self.export().copy()
info['directive'] = Directive.EditValidator info[ "directive" ] = Directive.EditValidator
info['validatorAddress'] = info.pop('validator-addr') # change the key info[ "validatorAddress" ] = info.pop(
info['nonce'] = nonce "validator-addr"
info['gasPrice'] = gas_price ) # change the key
info['gasLimit'] = gas_limit info[ "nonce" ] = nonce
_ = info.pop('max-rate') # not needed info[ "gasPrice" ] = gas_price
_ = info.pop('max-change-rate') # not needed info[ "gasLimit" ] = gas_limit
_ = info.pop('bls-public-keys') # remove this list _ = info.pop( "max-rate" ) # not needed
_ = info.pop('amount') # also unused _ = info.pop( "max-change-rate" ) # not needed
info['bls-key-to-remove'] = bls_key_to_remove _ = info.pop( "bls-public-keys" ) # remove this list
info['bls-key-to-add'] = bls_key_to_add _ = info.pop( "amount" ) # also unused
info[ "bls-key-to-remove" ] = bls_key_to_remove
info[ "bls-key-to-add" ] = bls_key_to_add
info[ "bls-key-to-add-sig" ] = bls_key_to_add_sig
if chain_id: if chain_id:
info['chainId'] = chain_id info[ "chainId" ] = chain_id
return sign_staking_transaction(info, private_key) return sign_staking_transaction( info, private_key )

@ -1,3 +1,2 @@
[pytest] [pytest]
addopts = -v --showlocals addopts = -v --showlocals
python_paths = .

@ -27,7 +27,7 @@ install_requires =
requests requests
incremental incremental
eth-rlp eth-rlp
eth-account == 0.5.4 eth-account >= 0.5.5
eth-utils eth-utils
hexbytes hexbytes
cytoolz cytoolz

@ -0,0 +1,578 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 105,
"id": "feee22ef",
"metadata": {},
"outputs": [],
"source": [
"from pyhmy import signing, staking_signing, numbers, transaction, account, validator as validator_module, staking_structures, contract\n",
"from web3 import Web3"
]
},
{
"cell_type": "code",
"execution_count": 3,
"id": "55b8db60",
"metadata": {},
"outputs": [],
"source": [
"# we need five transactions in conftest\n",
"# simple transfer (from localnet address)\n",
"# contract creation (from second address)\n",
"# cross shard transfer (from second address)\n",
"# validator creation (from localnet address)\n",
"# delegation (from second address)"
]
},
{
"cell_type": "markdown",
"id": "e104724c",
"metadata": {},
"source": [
"### Simple Transfer"
]
},
{
"cell_type": "code",
"execution_count": 144,
"id": "d7fa35f8",
"metadata": {},
"outputs": [],
"source": [
"pk = \"1f84c95ac16e6a50f08d44c7bde7aff8742212fda6e4321fde48bf83bef266dc\"\n",
"tx = {\n",
" 'from': 'one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3',\n",
" # 3c86ac59f6b038f584be1c08fced78d7c71bb55d5655f81714f3cddc82144c65\n",
" 'to': 'one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37',\n",
" 'gasPrice': Web3.toWei( 100, 'gwei' ),\n",
" 'gas': 21000,\n",
" 'chainId': 2, # localnet\n",
" 'value': int( numbers.convert_one_to_atto( 503 ) ),\n",
" 'nonce': 0,\n",
" 'shardID': 0,\n",
" 'toShardID': 0,\n",
"}\n",
"raw_tx = signing.sign_transaction(tx, pk).rawTransaction.hex()\n",
"tx_hash = transaction.send_raw_transaction(raw_tx)"
]
},
{
"cell_type": "code",
"execution_count": 145,
"id": "ed907d4b",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0xf86f8085174876e8008252088080941f2213a52f7409ff4f103458e6d202e0b3aa805a891b4486fafde57c00008027a0d7c0b20207dcc9dde376822dc3f5625eac6f59a7526111695cdba3e29553ca17a05d4ca9a421ae16f89cbf6848186eaea7a800da732446dff9952e7c1e91d414e3\n",
"0xc26be5776aa57438bccf196671a2d34f3f22c9c983c0f844c62b2fb90403aa43\n"
]
}
],
"source": [
"print( raw_tx )\n",
"print( tx_hash )"
]
},
{
"cell_type": "markdown",
"id": "1bbee37b",
"metadata": {},
"source": [
"### Contract Creation"
]
},
{
"cell_type": "code",
"execution_count": 147,
"id": "b143507b",
"metadata": {},
"outputs": [],
"source": [
"pk = '3c86ac59f6b038f584be1c08fced78d7c71bb55d5655f81714f3cddc82144c65'\n",
"data = \"0x6080604052348015600f57600080fd5b50607780601d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634936cd3614602d575b600080fd5b604080516001815290519081900360200190f3fea2646970667358221220fa3fa0e8d0267831a59f4dd5edf39a513d07e98461cb06660ad28d4beda744cd64736f6c634300080f0033\"\n",
"tx = {\n",
" 'gasPrice': Web3.toWei( 100, 'gwei' ),\n",
" 'gas': 100000,\n",
" 'chainId': 2,\n",
" 'nonce': 0,\n",
" 'shardID': 0,\n",
" 'toShardID': 0,\n",
" 'data': data,\n",
"}\n",
"raw_tx = signing.sign_transaction(tx, pk).rawTransaction.hex()\n",
"tx_hash = transaction.send_raw_transaction(raw_tx)"
]
},
{
"cell_type": "code",
"execution_count": 148,
"id": "53dbcbff",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0xf8e88085174876e800830186a080808080b8946080604052348015600f57600080fd5b50607780601d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634936cd3614602d575b600080fd5b604080516001815290519081900360200190f3fea2646970667358221220fa3fa0e8d0267831a59f4dd5edf39a513d07e98461cb06660ad28d4beda744cd64736f6c634300080f003327a08bf26ee0120c296b17af507f62606abdb5c5f09a65642c3d30b349b8bfbb3d69a03ec7be51c615bcbf2f1d63f6eaa56cf8d7be81671717f90239619830a81ebc9f\n",
"0xa605852dd2fa39ed42e101c17aaca9d344d352ba9b24b14b9af94ec9cb58b31f\n"
]
}
],
"source": [
"print( raw_tx )\n",
"print( tx_hash )"
]
},
{
"cell_type": "code",
"execution_count": 130,
"id": "6e66392b",
"metadata": {},
"outputs": [],
"source": [
"contract_address = transaction.get_transaction_receipt( tx_hash ).get( 'contractAddress' )\n",
"deployed = contract.get_code( contract_address, 'latest' )"
]
},
{
"cell_type": "code",
"execution_count": 131,
"id": "ead2f9d4",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0x6080604052348015600f57600080fd5b506004361060285760003560e01c80634936cd3614602d575b600080fd5b604080516001815290519081900360200190f3fea2646970667358221220fa3fa0e8d0267831a59f4dd5edf39a513d07e98461cb06660ad28d4beda744cd64736f6c634300080f0033\n",
"0x6080604052348015600f57600080fd5b50607780601d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634936cd3614602d575b600080fd5b604080516001815290519081900360200190f3fea2646970667358221220fa3fa0e8d0267831a59f4dd5edf39a513d07e98461cb06660ad28d4beda744cd64736f6c634300080f0033\n"
]
}
],
"source": [
"print( deployed )\n",
"print( data )"
]
},
{
"cell_type": "code",
"execution_count": 132,
"id": "453a34d6",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"3300f080003436c6f63746dc447adeb4d82da06660bc16489e70d315a93fde5dd4f95a1387620d8e0af3af0221228537660796462aef3f091002063009180915092518100615080406b5df080006b575d2064163dc63943608c10e065300067582060163400605b5df08000675f0065108432504060806x0\n",
"3300f080003436c6f63746dc447adeb4d82da06660bc16489e70d315a93fde5dd4f95a1387620d8e0af3af0221228537660796462aef3f091002063009180915092518100615080406b5df080006b575d2064163dc63943608c10e065300067582060163400605b5df08000675f0065108432504060806ef3f0006930006d10608770605b5df08000675f0065108432504060806x0\n"
]
}
],
"source": [
"print( \"\".join( [ deployed[ len( deployed ) - ( i + 1 ) ] for i in range( len( deployed ) ) ] ) )\n",
"print( \"\".join( [ data[ len( data ) - ( i + 1 ) ] for i in range( len( data ) ) ] ) )"
]
},
{
"cell_type": "code",
"execution_count": 133,
"id": "d251d1bf",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"True"
]
},
"execution_count": 133,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"\"0x6080604052348015600f57600080fd5b506004361060285760003560e01c80634936cd3614602d575b600080fd5b604080516001815290519081900360200190f3fea2646970667358221220fa3fa0e8d0267831a59f4dd5edf39a513d07e98461cb06660ad28d4beda744cd64736f6c634300080f0033\" == deployed"
]
},
{
"cell_type": "markdown",
"id": "812e033c",
"metadata": {},
"source": [
"### Cross Shard Transfer"
]
},
{
"cell_type": "code",
"execution_count": 149,
"id": "d7c70614",
"metadata": {},
"outputs": [],
"source": [
"pk = '3c86ac59f6b038f584be1c08fced78d7c71bb55d5655f81714f3cddc82144c65'\n",
"tx = {\n",
" 'from': 'one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37',\n",
" 'gasPrice': Web3.toWei( 100, 'gwei' ),\n",
" 'gas': 21000,\n",
" 'chainId': 2,\n",
" 'nonce': 1,\n",
" 'shardID': 0,\n",
" 'toShardID': 1,\n",
" 'to': 'one1e8rdglh97t37prtnv7k35ymnh2wazujpzsmzes',\n",
" 'value': Web3.toWei( 100, 'gwei' ),\n",
"}\n",
"raw_tx = signing.sign_transaction(tx, pk).rawTransaction.hex()\n",
"tx_hash = transaction.send_raw_transaction(raw_tx)"
]
},
{
"cell_type": "code",
"execution_count": 150,
"id": "f20990f1",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0xf86b0185174876e800825208800194c9c6d47ee5f2e3e08d7367ad1a1373ba9dd1724185174876e8008027a02501c517220e9499f14e97c20b0a88cd3b7ba80637bba43ed295422e69a3f300a079b8e1213c9506184aed6ac2eb0b2cb00594c3f9fcdd6c088937ce17fe47107c\n",
"0xf73ba634cb96fc0e3e2c9d3b4c91379e223741be4a5aa56e6d6caf49c1ae75cf\n"
]
}
],
"source": [
"print( raw_tx )\n",
"print( tx_hash )"
]
},
{
"cell_type": "code",
"execution_count": 153,
"id": "66f024b9",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"0"
]
},
"execution_count": 153,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"account.get_balance( 'one1e8rdglh97t37prtnv7k35ymnh2wazujpzsmzes', 'http://localhost:9502' )"
]
},
{
"cell_type": "markdown",
"id": "2b2446df",
"metadata": {},
"source": [
"### Validator Creation"
]
},
{
"cell_type": "code",
"execution_count": 154,
"id": "c3513c37",
"metadata": {},
"outputs": [],
"source": [
"pk = \"1f84c95ac16e6a50f08d44c7bde7aff8742212fda6e4321fde48bf83bef266dc\"\n",
"address = \"one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3\"\n",
"info = {\n",
" \"name\": \"Alice\",\n",
" \"identity\": \"alice\",\n",
" \"website\": \"alice.harmony.one\",\n",
" \"security-contact\": \"Bob\",\n",
" \"details\": \"Are you even reading this?\",\n",
" \"min-self-delegation\": int( numbers.convert_one_to_atto( 10000 ) ),\n",
" \"max-total-delegation\": int( numbers.convert_one_to_atto( 100000 ) ),\n",
" \"rate\": \"0.1\",\n",
" \"max-rate\": \"0.9\",\n",
" \"max-change-rate\": \"0.05\",\n",
" \"bls-public-keys\": [\n",
" # private key is b1f2a5029f5f43c8c933a61ce936ced030b2c9379f8e2478fc888fa670cdbc89b8cd1ebc29b5b00a81d3152bb3aaa3a337404f50bee5e434430ca3693a94a1c102a765cf3b0887b8b0bcf5317d33f4bec60a97feae2498a39ab7a1c2\n",
" # blspass.txt is empty\n",
" \"0xa20e70089664a874b00251c5e85d35a73871531306f3af43e02138339d294e6bb9c4eb82162199c6a852afeaa8d68712\",\n",
" ],\n",
" \"amount\": int( numbers.convert_one_to_atto( 10000 ) ),\n",
" \"bls-key-sigs\": [\n",
" \"0xef2c49a2f31fbbd23c21bc176eaf05cd0bebe6832033075d81fea7cff6f9bc1ab42f3b6895c5493fe645d8379d2eaa1413de55a9d3ce412a4f747cb57d52cc4da4754bfb2583ec9a41fe5dd48287f964f276336699959a5fcef3391dc24df00d\",\n",
" ]\n",
"}\n",
"validator = validator_module.Validator( address )\n",
"validator.load( info )\n",
"raw_tx = validator.sign_create_validator_transaction(\n",
" 1,\n",
" Web3.toWei( 100, 'gwei' ),\n",
" 55000000, # gas limit\n",
" pk,\n",
" 2 # chain id\n",
").rawTransaction.hex()\n",
"tx_hash = transaction.send_raw_staking_transaction(\n",
" raw_tx,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 155,
"id": "9b12f75f",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0xf9017c80f9012994a5241513da9f4463f1d4874b548dfbac29d91f34f83d85416c69636585616c69636591616c6963652e6861726d6f6e792e6f6e6583426f629a41726520796f75206576656e2072656164696e6720746869733fddc988016345785d8a0000c9880c7d713b49da0000c887b1a2bc2ec500008a021e19e0c9bab24000008a152d02c7e14af6800000f1b0a20e70089664a874b00251c5e85d35a73871531306f3af43e02138339d294e6bb9c4eb82162199c6a852afeaa8d68712f862b860ef2c49a2f31fbbd23c21bc176eaf05cd0bebe6832033075d81fea7cff6f9bc1ab42f3b6895c5493fe645d8379d2eaa1413de55a9d3ce412a4f747cb57d52cc4da4754bfb2583ec9a41fe5dd48287f964f276336699959a5fcef3391dc24df00d8a021e19e0c9bab24000000185174876e8008403473bc028a08c1146305eaef981aa24c2f17c8519664d10c99ee42acedbc258749930d31a7ca031dadf114ee6ab9bd09933208094c65037b66c796bcfc57a70158106b37357b0\n",
"0x400e9831d358f5daccd153cad5bf53650a0d413bd8682ec0ffad55367d162968\n"
]
}
],
"source": [
"print( raw_tx )\n",
"print( tx_hash )"
]
},
{
"cell_type": "markdown",
"id": "4c2ff645",
"metadata": {},
"source": [
"### Delegation"
]
},
{
"cell_type": "code",
"execution_count": 156,
"id": "458d81b8",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0xf88302f4941f2213a52f7409ff4f103458e6d202e0b3aa805a94a5241513da9f4463f1d4874b548dfbac29d91f3489056bc75e2d631000000285174876e80082c35028a02c5e953062dcdfa2de9723639b63bab45705eb6dfbfe7f44536ed266c3c7ca20a0742964e646338e7431874f70715565d99c01c762324355c69db34a9ed9de81d7\n",
"0xc8177ace2049d9f4eb4a45fd6bd6b16f693573d036322c36774cc00d05a3e24f\n"
]
}
],
"source": [
"pk = \"3c86ac59f6b038f584be1c08fced78d7c71bb55d5655f81714f3cddc82144c65\"\n",
"tx = {\n",
" 'directive': staking_structures.Directive.Delegate,\n",
" 'delegatorAddress': 'one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37',\n",
" 'validatorAddress': 'one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3',\n",
" 'amount': Web3.toWei( 100, 'ether' ),\n",
" 'nonce': 2,\n",
" 'gasPrice': Web3.toWei( 100, 'gwei' ),\n",
" 'gasLimit': 50000,\n",
" 'chainId': 2,\n",
"}\n",
"raw_tx = staking_signing.sign_staking_transaction(\n",
" tx,\n",
" pk,\n",
").rawTransaction.hex()\n",
"tx_hash = transaction.send_raw_staking_transaction(\n",
" raw_tx,\n",
")\n",
"print( raw_tx )\n",
"print( tx_hash )"
]
},
{
"cell_type": "markdown",
"id": "8efa5536",
"metadata": {},
"source": [
"### test_transaction.py - transfer 105 ONE to another address"
]
},
{
"cell_type": "code",
"execution_count": 157,
"id": "c3295fee",
"metadata": {},
"outputs": [],
"source": [
"pk = '3c86ac59f6b038f584be1c08fced78d7c71bb55d5655f81714f3cddc82144c65'\n",
"tx = {\n",
" 'from': 'one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37',\n",
" 'gasPrice': Web3.toWei( 100, 'gwei' ),\n",
" 'gas': 21000,\n",
" 'chainId': 2,\n",
" 'nonce': 3,\n",
" 'shardID': 0,\n",
" 'toShardID': 0,\n",
" 'to': 'one1e8rdglh97t37prtnv7k35ymnh2wazujpzsmzes',\n",
" 'value': Web3.toWei( 105, 'ether' ),\n",
"}\n",
"raw_tx = signing.sign_transaction(tx, pk).rawTransaction.hex()\n",
"tx_hash = transaction.send_raw_transaction(raw_tx)"
]
},
{
"cell_type": "code",
"execution_count": 158,
"id": "af515c7e",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0xf86f0385174876e800825208808094c9c6d47ee5f2e3e08d7367ad1a1373ba9dd172418905b12aefafa80400008027a07a4952b90bf38723a9197179a8e6d2e9b3a86fd6da4e66a9cf09fdc59783f757a053910798b311245525bd77d6119332458c2855102e4fb9e564f6a3b710d18bb0\n",
"0x7ccd80f8513f76ec58b357c7a82a12a95e025d88f1444e953f90e3d86e222571\n"
]
}
],
"source": [
"print( raw_tx )\n",
"print( tx_hash )"
]
},
{
"cell_type": "markdown",
"id": "bb546b3e",
"metadata": {},
"source": [
"### test_transaction.py - staking transaction"
]
},
{
"cell_type": "code",
"execution_count": 168,
"id": "c14e2d6d",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"0xf88302f494c9c6d47ee5f2e3e08d7367ad1a1373ba9dd1724194a5241513da9f4463f1d4874b548dfbac29d91f3489056bc75e2d631000008085174876e80082c35027a0808ea7d27adf3b1f561e8da4676814084bb75ac541b616bece87c6446e6cc54ea02f19f0b14240354bd42ad60b0c7189873c0be87044e13072b0981a837ca76f64\n",
"0xe7d07ef6d9fca595a14ceb0ca917bece7bedb15efe662300e9334a32ac1da629\n"
]
}
],
"source": [
"pk = \"ff9ef6b00a61672b4b7bedd5ac653439b56ac8ee808c99a1bd871cf51b7d60eb\"\n",
"tx = {\n",
" 'directive': staking_structures.Directive.Delegate,\n",
" 'delegatorAddress': 'one1e8rdglh97t37prtnv7k35ymnh2wazujpzsmzes',\n",
" 'validatorAddress': 'one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3',\n",
" 'amount': Web3.toWei( 100, 'ether' ),\n",
" 'nonce': 0,\n",
" 'gasPrice': Web3.toWei( 100, 'gwei' ),\n",
" 'gasLimit': 50000,\n",
" 'chainId': 2,\n",
"}\n",
"raw_tx = staking_signing.sign_staking_transaction(\n",
" tx,\n",
" pk,\n",
").rawTransaction.hex()\n",
"tx_hash = transaction.send_raw_staking_transaction(\n",
" raw_tx,\n",
")\n",
"print( raw_tx )\n",
"print( tx_hash )"
]
},
{
"cell_type": "code",
"execution_count": 162,
"id": "ebf296aa",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'blockHash': '0xf55f1fb3c9be76fb74370e8a7d8580327797d2d6082040074783207a171e2de6',\n",
" 'blockNumber': 34,\n",
" 'from': 'one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37',\n",
" 'hash': '0xf73ba634cb96fc0e3e2c9d3b4c91379e223741be4a5aa56e6d6caf49c1ae75cf',\n",
" 'shardID': 0,\n",
" 'to': 'one1e8rdglh97t37prtnv7k35ymnh2wazujpzsmzes',\n",
" 'toShardID': 1,\n",
" 'value': 100000000000}"
]
},
"execution_count": 162,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"transaction.get_cx_receipt_by_hash( '0xf73ba634cb96fc0e3e2c9d3b4c91379e223741be4a5aa56e6d6caf49c1ae75cf', 'http://localhost:9502' )"
]
},
{
"cell_type": "code",
"execution_count": 166,
"id": "ff0229ce",
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"{'blockHash': '0x0000000000000000000000000000000000000000000000000000000000000000',\n",
" 'blockNumber': None,\n",
" 'from': 'one1e8rdglh97t37prtnv7k35ymnh2wazujpzsmzes',\n",
" 'gas': 50000,\n",
" 'gasPrice': 100000000000,\n",
" 'hash': '0x279935278d20d778cbe4fdfa5d51be9eb1eb184053dc9a7cb88ad3365df73060',\n",
" 'msg': {'amount': 100000000000000000000,\n",
" 'delegatorAddress': 'one1e8rdglh97t37prtnv7k35ymnh2wazujpzsmzes',\n",
" 'validatorAddress': 'one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3'},\n",
" 'nonce': 2,\n",
" 'r': '0x8660a63c10af06f2fb3f24b92cf61d4f319044a1f1931c4f4e54ce986ff563c',\n",
" 's': '0x597785559c4283d3ece2df37cbf37077966487a2a2dc0f4cdbbf75a8f20bc1a8',\n",
" 'timestamp': 0,\n",
" 'transactionIndex': 0,\n",
" 'type': 'Delegate',\n",
" 'v': '0x27'}"
]
},
"execution_count": 166,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"transaction.get_staking_transaction_by_hash( \"0x279935278d20d778cbe4fdfa5d51be9eb1eb184053dc9a7cb88ad3365df73060\" )"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.5"
}
},
"nbformat": 4,
"nbformat_minor": 5
}

@ -1,9 +1,9 @@
from pyhmy.bech32 import ( from pyhmy.bech32 import bech32
bech32
)
def test_encode(): def test_encode():
bech32.encode('one', 5, [121, 161]) bech32.encode( "one", 5, [ 121, 161 ] )
def test_decode(): def test_decode():
bech32.decode('one', 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9') bech32.decode( "one", "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9" )

@ -10,61 +10,58 @@ TEMP_DIR = "/tmp/pyhmy-testing/test-cli"
BINARY_FILE_PATH = f"{TEMP_DIR}/bin/cli_test_binary" BINARY_FILE_PATH = f"{TEMP_DIR}/bin/cli_test_binary"
@pytest.fixture(scope="session", autouse=True) @pytest.fixture( scope = "session", autouse = True )
def setup(): def setup():
shutil.rmtree(TEMP_DIR, ignore_errors=True) shutil.rmtree( TEMP_DIR, ignore_errors = True )
os.makedirs(TEMP_DIR, exist_ok=True) os.makedirs( TEMP_DIR, exist_ok = True )
@pytest.mark.run(order=0)
def test_download_cli(): def test_download_cli():
env = cli.download(BINARY_FILE_PATH, replace=False, verbose=False) env = cli.download( BINARY_FILE_PATH, replace = False, verbose = False )
cli.environment.update(env) cli.environment.update( env )
assert os.path.exists(BINARY_FILE_PATH) assert os.path.exists( BINARY_FILE_PATH )
@pytest.mark.run(order=1)
def test_is_valid(): def test_is_valid():
bad_file_path = os.path.realpath(f"{TEMP_DIR}/test_is_valid/bad_hmy") bad_file_path = os.path.realpath( f"{TEMP_DIR}/test_is_valid/bad_hmy" )
shutil.rmtree(Path(bad_file_path).parent, ignore_errors=True) shutil.rmtree( Path( bad_file_path ).parent, ignore_errors = True )
os.makedirs(Path(bad_file_path).parent, exist_ok=True) os.makedirs( Path( bad_file_path ).parent, exist_ok = True )
Path(bad_file_path).touch() Path( bad_file_path ).touch()
assert os.path.exists(BINARY_FILE_PATH), "harmony cli did not download" assert os.path.exists( BINARY_FILE_PATH ), "harmony cli did not download"
assert os.path.exists(bad_file_path), "did not create bad binary" assert os.path.exists( bad_file_path ), "did not create bad binary"
assert cli.is_valid_binary(BINARY_FILE_PATH) assert cli.is_valid_binary( BINARY_FILE_PATH )
assert not cli.is_valid_binary(bad_file_path) assert not cli.is_valid_binary( bad_file_path )
@pytest.mark.run(order=2)
def test_bad_bin_set(): def test_bad_bin_set():
bad_file_path = os.path.realpath(f"{TEMP_DIR}/test_bad_bin_set/hmy") bad_file_path = os.path.realpath( f"{TEMP_DIR}/test_bad_bin_set/hmy" )
shutil.rmtree(Path(bad_file_path).parent, ignore_errors=True) shutil.rmtree( Path( bad_file_path ).parent, ignore_errors = True )
os.makedirs(Path(bad_file_path).parent, exist_ok=True) os.makedirs( Path( bad_file_path ).parent, exist_ok = True )
Path(bad_file_path).touch() Path( bad_file_path ).touch()
is_set = cli.set_binary(bad_file_path) is_set = cli.set_binary( bad_file_path )
assert not is_set assert not is_set
assert cli.get_binary_path() != bad_file_path assert cli.get_binary_path() != bad_file_path
@pytest.mark.run(order=3)
def test_bin_set(): def test_bin_set():
cli.set_binary(BINARY_FILE_PATH) cli.set_binary( BINARY_FILE_PATH )
cli_binary_path = cli.get_binary_path() cli_binary_path = cli.get_binary_path()
assert os.path.realpath(cli_binary_path) == os.path.realpath(BINARY_FILE_PATH) assert os.path.realpath( cli_binary_path
) == os.path.realpath( BINARY_FILE_PATH )
def test_update_keystore(): def test_update_keystore():
cli.single_call("hmy keys add test1") cli.single_call( "hmy keys add test1" )
addrs = cli.get_accounts_keystore() addrs = cli.get_accounts_keystore()
assert "test1" in addrs.keys() assert "test1" in addrs.keys()
check_addr = addrs["test1"] check_addr = addrs[ "test1" ]
accounts_list = cli.get_accounts(check_addr) accounts_list = cli.get_accounts( check_addr )
check_acc = accounts_list[0] check_acc = accounts_list[ 0 ]
assert check_acc == "test1" assert check_acc == "test1"
raw_cli_keys_list_print = cli.single_call("hmy keys list", timeout=2) raw_cli_keys_list_print = cli.single_call( "hmy keys list", timeout = 2 )
assert check_addr in raw_cli_keys_list_print assert check_addr in raw_cli_keys_list_print
assert check_acc in raw_cli_keys_list_print assert check_acc in raw_cli_keys_list_print
assert addrs[check_acc] == check_addr assert addrs[ check_acc ] == check_addr
cli.remove_address(check_addr) cli.remove_address( check_addr )
assert check_addr not in addrs.values() assert check_addr not in addrs.values()
assert "test1" not in addrs.keys() assert "test1" not in addrs.keys()

@ -6,19 +6,19 @@ from pyhmy import logging
def test_basic_logger(): def test_basic_logger():
if os.path.exists(f"{os.getcwd()}/logs/pytest.log"): if os.path.exists( f"{os.getcwd()}/logs/pytest.log" ):
os.remove(f"{os.getcwd()}/logs/pytest.log") os.remove( f"{os.getcwd()}/logs/pytest.log" )
logger = logging.ControlledLogger("pytest", "logs/") logger = logging.ControlledLogger( "pytest", "logs/" )
assert os.path.exists(f"{os.getcwd()}/logs/pytest.log") assert os.path.exists( f"{os.getcwd()}/logs/pytest.log" )
logger.info("test info") logger.info( "test info" )
logger.debug("test debug") logger.debug( "test debug" )
logger.error("test error") logger.error( "test error" )
logger.warning("test warning") logger.warning( "test warning" )
with open(f"{os.getcwd()}/logs/pytest.log", 'r') as f: with open( f"{os.getcwd()}/logs/pytest.log", "r" ) as f:
log_file_contents = f.readlines() log_file_contents = f.readlines()
assert not log_file_contents assert not log_file_contents
logger.write() logger.write()
with open(f"{os.getcwd()}/logs/pytest.log", 'r') as f: with open( f"{os.getcwd()}/logs/pytest.log", "r" ) as f:
log_file_contents = f.readlines() log_file_contents = f.readlines()
for line in log_file_contents: for line in log_file_contents:
if "INFO" in line: if "INFO" in line:

@ -1,36 +1,31 @@
from decimal import Decimal from decimal import Decimal
import pytest from pyhmy import numbers
from pyhmy import (
numbers
)
@pytest.mark.run(order=1)
def test_convert_atto_to_one(): def test_convert_atto_to_one():
a = numbers.convert_atto_to_one(1e18) a = numbers.convert_atto_to_one( 1e18 )
assert Decimal(1) == a assert Decimal( 1 ) == a
b = numbers.convert_atto_to_one( 1e18 + 0.6 )
assert Decimal( 1 ) == b
b = numbers.convert_atto_to_one(1e18 + 0.6) c = numbers.convert_atto_to_one( "1" + ( "0" * 18 ) )
assert Decimal(1) == b assert Decimal( 1 ) == c
c = numbers.convert_atto_to_one('1' + ('0' * 18)) d = numbers.convert_atto_to_one( Decimal( 1e18 ) )
assert Decimal(1) == c assert Decimal( 1 ) == d
d = numbers.convert_atto_to_one(Decimal(1e18))
assert Decimal(1) == d
@pytest.mark.run(order=2)
def test_convert_one_to_atto(): def test_convert_one_to_atto():
a = numbers.convert_one_to_atto(1e-18) a = numbers.convert_one_to_atto( 1e-18 )
assert Decimal(1) == a assert Decimal( 1 ) == a
b = numbers.convert_one_to_atto(1.5) b = numbers.convert_one_to_atto( 1.5 )
assert Decimal(1.5e18) == b assert Decimal( 1.5e18 ) == b
c = numbers.convert_one_to_atto('1') c = numbers.convert_one_to_atto( "1" )
assert Decimal(1e18) == c assert Decimal( 1e18 ) == c
d = numbers.convert_one_to_atto(Decimal(1)) d = numbers.convert_one_to_atto( Decimal( 1 ) )
assert Decimal(1e18) == d assert Decimal( 1e18 ) == d

@ -4,17 +4,14 @@ import socket
import pytest import pytest
import requests import requests
from pyhmy.rpc import ( from pyhmy.rpc import exceptions, request
exceptions,
request
)
@pytest.fixture(scope="session", autouse=True) @pytest.fixture( scope = "session", autouse = True )
def setup(): def setup():
endpoint = 'http://localhost:9500' endpoint = "http://localhost:9500"
timeout = 30 timeout = 30
method = 'hmyv2_getNodeMetadata' method = "hmyv2_getNodeMetadata"
params = [] params = []
payload = { payload = {
"id": "1", "id": "1",
@ -23,52 +20,64 @@ def setup():
"params": params "params": params
} }
headers = { headers = {
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
try: try:
response = requests.request('POST', endpoint, headers=headers, response = requests.request(
data=json.dumps(payload), timeout=timeout, allow_redirects=True) "POST",
endpoint,
headers = headers,
data = json.dumps( payload ),
timeout = timeout,
allow_redirects = True,
)
except Exception as e: except Exception as e:
pytest.skip("can not connect to local blockchain", allow_module_level=True) pytest.skip(
"can not connect to local blockchain",
allow_module_level = True
)
@pytest.mark.run(order=1)
def test_request_connection_error(): def test_request_connection_error():
# Find available port # Find available port
s = socket.socket() s = socket.socket()
s.bind(('localhost', 0)) s.bind( ( "localhost", 0 ) )
port = s.getsockname()[1] port = s.getsockname()[ 1 ]
s.close() s.close()
if port == 0: if port == 0:
pytest.skip("could not find available port") pytest.skip( "could not find available port" )
bad_endpoint = f'http://localhost:{port}' bad_endpoint = f"http://localhost:{port}"
bad_request = None bad_request = None
try: try:
bad_request = request.rpc_request('hmyv2_getNodeMetadata', endpoint=bad_endpoint) bad_request = request.rpc_request(
"hmyv2_getNodeMetadata",
endpoint = bad_endpoint
)
except Exception as e: except Exception as e:
assert isinstance(e, exceptions.RequestsError) assert isinstance( e, exceptions.RequestsError )
assert bad_request is None assert bad_request is None
@pytest.mark.run(order=2)
def test_request_rpc_error(): def test_request_rpc_error():
error_request = None error_request = None
try: try:
error_request = request.rpc_request('hmyv2_getBalance') error_request = request.rpc_request( "hmyv2_getBalance" )
except (exceptions.RequestsTimeoutError, exceptions.RequestsError) as err: except ( exceptions.RequestsTimeoutError, exceptions.RequestsError ) as err:
pytest.skip("can not connect to local blockchain", allow_module_level=True) pytest.skip(
"can not connect to local blockchain",
allow_module_level = True
)
except Exception as e: except Exception as e:
assert isinstance(e, exceptions.RPCError) assert isinstance( e, exceptions.RPCError )
assert error_request is None assert error_request is None
@pytest.mark.run(order=3)
def test_rpc_request(): def test_rpc_request():
endpoint = 'http://localhost:9500' endpoint = "http://localhost:9500"
timeout = 30 timeout = 30
method = 'hmyv2_getNodeMetadata' method = "hmyv2_getNodeMetadata"
params = [] params = []
payload = { payload = {
"id": "1", "id": "1",
@ -77,29 +86,35 @@ def test_rpc_request():
"params": params "params": params
} }
headers = { headers = {
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
response = None response = None
try: try:
response = requests.request('POST', endpoint, headers=headers, response = requests.request(
data=json.dumps(payload), timeout=timeout, allow_redirects=True) "POST",
endpoint,
headers = headers,
data = json.dumps( payload ),
timeout = timeout,
allow_redirects = True,
)
except: except:
pytest.skip("can not connect to local blockchain") pytest.skip( "can not connect to local blockchain" )
assert response is not None assert response is not None
resp = None resp = None
try: try:
resp = json.loads(response.content) resp = json.loads( response.content )
except json.decoder.JSONDecodeError as err: except json.decoder.JSONDecodeError as err:
pytest.skip('unable to decode response') pytest.skip( "unable to decode response" )
assert resp is not None assert resp is not None
rpc_response = None rpc_response = None
try: try:
rpc_response = request.rpc_request(method, params, endpoint, timeout) rpc_response = request.rpc_request( method, params, endpoint, timeout )
except exceptions.RPCError as e: except exceptions.RPCError as e:
assert 'error' in resp assert "error" in resp
if rpc_response is not None: if rpc_response is not None:
assert rpc_response == resp assert rpc_response == resp

@ -1,72 +1,74 @@
import json import json
import time import time
import random
import pytest import pytest
import requests import requests
test_validator_address = 'one18tvf56zqjkjnak686lwutcp5mqfnvee35xjnhc' # private keys
transfer_raw_transaction = '0xf86f80843b9aca008252080180943ad89a684095a53edb47d7ddc5e034d8133667318a152d02c7e14af68000008027a0ec6c8ad0f70b3c826fa77574c6815a8f73936fafb7b2701a7082ad7d278c95a9a0429f9f166b1c1d385a4ec8f8b86604c26e427c2b0a1c85d9cf4ec6bbd0719508' # 1f84c95ac16e6a50f08d44c7bde7aff8742212fda6e4321fde48bf83bef266dc / one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3 (genesis)
tx_hash = '0x1fa20537ea97f162279743139197ecf0eac863278ac1c8ada9a6be5d1e31e633' # 3c86ac59f6b038f584be1c08fced78d7c71bb55d5655f81714f3cddc82144c65 / one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37 (transferred 503)
create_validator_raw_transaction = '0xf9015680f90105943ad89a684095a53edb47d7ddc5e034d813366731d984746573748474657374847465737484746573748474657374ddc988016345785d8a0000c9880c7d713b49da0000c887b1a2bc2ec500008a022385a827e8155000008b084595161401484a000000f1b0282554f2478661b4844a05a9deb1837aac83931029cb282872f0dcd7239297c499c02ea8da8746d2f08ca2b037e89891f862b86003557e18435c201ecc10b1664d1aea5b4ec59dbfe237233b953dbd9021b86bc9770e116ed3c413fe0334d89562568a10e133d828611f29fee8cdab9719919bbcc1f1bf812c73b9ccd0f89b4f0b9ca7e27e66d58bbb06fcf51c295b1d076cfc878a0228f16f86157860000080843b9aca008351220027a018385211a150ca032c3526cef0aba6a75f99a18cb73f547f67bab746be0c7a64a028be921002c6eb949b3932afd010dfe1de2459ec7fe84403b9d9d8892394a78c'
staking_tx_hash = '0x57ec011aabdeb078a4816502224022f291fa8b07c82bbae8476f514a1d71c730'
contract_tx_hash = '0xa13414dd152173395c69a11e79dea31bf029660f747a42a53744181d05571e70'
contract_raw_transaction = '0xf9025080843b9aca008366916c80808080b901fc608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555061019c806100606000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063445df0ac146100465780638da5cb5b14610064578063fdacd576146100ae575b600080fd5b61004e6100dc565b6040518082815260200191505060405180910390f35b61006c6100e2565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100da600480360360208110156100c457600080fd5b8101908080359060200190929190505050610107565b005b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561016457806001819055505b5056fea265627a7a723158209b80813a158b44af65aee232b44c0ac06472c48f4abbe298852a39f0ff34a9f264736f6c6343000510003227a03a3ad2b7c2934a8325fc04d04daad740d337bb1f589482bbb1d091e1be804d29a00c46772871866a34f254e6197a526bebc2067f75edc53c488b31d84e07c3c685'
endpoint = 'http://localhost:9500' endpoint = "http://localhost:9500"
endpoint_shard_one = 'http://localhost:9501'
timeout = 30 timeout = 30
headers = { headers = {
'Content-Type': 'application/json' "Content-Type": "application/json"
} }
txs = [
# same shard 503 ONE transfer from one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3 to one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37 (0 nonce)
"0xf86f8085174876e8008252088080941f2213a52f7409ff4f103458e6d202e0b3aa805a891b4486fafde57c00008027a0d7c0b20207dcc9dde376822dc3f5625eac6f59a7526111695cdba3e29553ca17a05d4ca9a421ae16f89cbf6848186eaea7a800da732446dff9952e7c1e91d414e3",
# contract creation by one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37 (0 nonce)
"0xf8e88085174876e800830186a080808080b8946080604052348015600f57600080fd5b50607780601d6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c80634936cd3614602d575b600080fd5b604080516001815290519081900360200190f3fea2646970667358221220fa3fa0e8d0267831a59f4dd5edf39a513d07e98461cb06660ad28d4beda744cd64736f6c634300080f003327a08bf26ee0120c296b17af507f62606abdb5c5f09a65642c3d30b349b8bfbb3d69a03ec7be51c615bcbf2f1d63f6eaa56cf8d7be81671717f90239619830a81ebc9f",
# cross shard transfer by one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37 (1 nonce)
"0xf86b0185174876e800825208800194c9c6d47ee5f2e3e08d7367ad1a1373ba9dd1724185174876e8008027a02501c517220e9499f14e97c20b0a88cd3b7ba80637bba43ed295422e69a3f300a079b8e1213c9506184aed6ac2eb0b2cb00594c3f9fcdd6c088937ce17fe47107c",
]
stxs = [
# creation of one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3 as validator (1 nonce)
"0xf9017c80f9012994a5241513da9f4463f1d4874b548dfbac29d91f34f83d85416c69636585616c69636591616c6963652e6861726d6f6e792e6f6e6583426f629a41726520796f75206576656e2072656164696e6720746869733fddc988016345785d8a0000c9880c7d713b49da0000c887b1a2bc2ec500008a021e19e0c9bab24000008a152d02c7e14af6800000f1b0a20e70089664a874b00251c5e85d35a73871531306f3af43e02138339d294e6bb9c4eb82162199c6a852afeaa8d68712f862b860ef2c49a2f31fbbd23c21bc176eaf05cd0bebe6832033075d81fea7cff6f9bc1ab42f3b6895c5493fe645d8379d2eaa1413de55a9d3ce412a4f747cb57d52cc4da4754bfb2583ec9a41fe5dd48287f964f276336699959a5fcef3391dc24df00d8a021e19e0c9bab24000000185174876e8008403473bc028a08c1146305eaef981aa24c2f17c8519664d10c99ee42acedbc258749930d31a7ca031dadf114ee6ab9bd09933208094c65037b66c796bcfc57a70158106b37357b0",
# delegation by one1ru3p8ff0wsyl7ncsx3vwd5szuze64qz60upg37 to one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3 (2 nonce)
"0xf88302f4941f2213a52f7409ff4f103458e6d202e0b3aa805a94a5241513da9f4463f1d4874b548dfbac29d91f3489056bc75e2d631000000285174876e80082c35028a02c5e953062dcdfa2de9723639b63bab45705eb6dfbfe7f44536ed266c3c7ca20a0742964e646338e7431874f70715565d99c01c762324355c69db34a9ed9de81d7",
]
tx_hashes = [
"0xc26be5776aa57438bccf196671a2d34f3f22c9c983c0f844c62b2fb90403aa43",
"0xa605852dd2fa39ed42e101c17aaca9d344d352ba9b24b14b9af94ec9cb58b31f",
"0xf73ba634cb96fc0e3e2c9d3b4c91379e223741be4a5aa56e6d6caf49c1ae75cf",
]
stx_hashes = [
"0x400e9831d358f5daccd153cad5bf53650a0d413bd8682ec0ffad55367d162968",
"0xc8177ace2049d9f4eb4a45fd6bd6b16f693573d036322c36774cc00d05a3e24f",
]
assert len( txs ) == len( tx_hashes ), "Mismatch in tx and tx_hash count"
assert len( stxs ) == len( stx_hashes ), "Mismatch in stx and stx_hash count"
@pytest.fixture(scope="session", autouse=True)
@pytest.fixture( scope = "session", autouse = True )
def setup_blockchain(): def setup_blockchain():
# return
metadata = _check_connection() metadata = _check_connection()
_check_staking_epoch(metadata) _check_staking_epoch( metadata )
tx_data = _check_funding_transaction()
if not tx_data['result']:
_send_funding_transaction()
time.sleep(20) # Sleep to let cross shard transaction finalize
tx_data = _check_funding_transaction()
if 'error' in tx_data:
pytest.skip(f"Error in hmyv2_getTransactionByHash reply: {tx_data['error']}", allow_module_level=True)
if not tx_data['result']:
pytest.skip(f"Funding transaction failed: {tx_hash}", allow_module_level=True)
stx_data = _check_staking_transaction()
if not stx_data['result']:
_send_staking_transaction()
time.sleep(30) # Sleep to let transaction finalize
stx_data = _check_staking_transaction()
if 'error' in stx_data:
pytest.skip(f"Error in hmyv2_getStakingTransactionByHash reply: {stx_data['error']}", allow_module_level=True)
if not stx_data['result']:
pytest.skip(f"Staking transaction failed: {staking_tx_hash}", allow_module_level=True)
contract_data = _check_contract_transaction() for i in range( len( txs ) ):
tx = txs[ i ]
tx_hash = tx_hashes[ i ]
_send_transaction( tx, endpoint )
if not _wait_for_transaction_confirmed( tx_hash, endpoint ):
pytest.skip(
"Could not confirm initial transaction #{} on chain"
.format( i ),
allow_module_level = True,
)
if not contract_data['result']: for i in range( len( stxs ) ):
_send_contract_transaction() stx = stxs[ i ]
times.sleep(30) stx_hash = stx_hashes[ i ]
_send_staking_transaction( stx, endpoint )
contract_data = _check_contract_transaction() if not _wait_for_staking_transaction_confirmed( stx_hash, endpoint ):
if 'error' in contract_data: pytest.skip(
pytest.skip(f"Error in hmyv2_getStakingTransactionByHash reply: {contract_data['error']}", allow_module_level=True) "Could not confirm initial staking transaction #{} on chain"
if not contract_data['result']: .format( i ),
pytest.skip(f"Staking transaction failed: {contract_tx_hash}", allow_module_level=True) allow_module_level = True,
)
# TODO: Build data object to return data instead of hard coded values in the test files
try:
return int(stx_data['result']['blockNumber'])
except (TypeError, KeyError) as e:
pytest.skip(f"Unexpected reply for hmyv2_getStakingTransactionByHash: {stx_data['result']}", allow_module_level=True)
def _check_connection(): def _check_connection():
@ -74,132 +76,206 @@ def _check_connection():
payload = { payload = {
"id": "1", "id": "1",
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": 'hmyv2_getNodeMetadata', "method": "hmyv2_getNodeMetadata",
"params": [] "params": [],
} }
response = requests.request('POST', endpoint, headers=headers, response = requests.request(
data=json.dumps(payload), timeout=timeout, allow_redirects=True) "POST",
metadata = json.loads(response.content) endpoint,
if 'error' in metadata: headers = headers,
pytest.skip(f"Error in hmyv2_getNodeMetadata reply: {metadata['error']}", allow_module_level=True) data = json.dumps( payload ),
if 'chain-config' not in metadata['result']: timeout = timeout,
pytest.skip("Chain config not found in hmyv2_getNodeMetadata reply", allow_module_level=True) allow_redirects = True,
)
metadata = json.loads( response.content )
if "error" in metadata:
pytest.skip(
f"Error in hmyv2_getNodeMetadata reply: {metadata['error']}",
allow_module_level = True,
)
if "chain-config" not in metadata[ "result" ]:
pytest.skip(
"Chain config not found in hmyv2_getNodeMetadata reply",
allow_module_level = True,
)
return metadata return metadata
except Exception as e: except Exception as e:
pytest.skip('Can not connect to local blockchain or bad hmyv2_getNodeMetadata reply', allow_module_level=True) pytest.skip(
"Can not connect to local blockchain or bad hmyv2_getNodeMetadata reply",
allow_module_level = True,
)
def _check_staking_epoch(metadata): def _check_staking_epoch( metadata ):
latest_header = None latest_header = None
try: try:
payload = { payload = {
"id": "1", "id": "1",
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": 'hmyv2_latestHeader', "method": "hmyv2_latestHeader",
"params": [] "params": [],
} }
response = requests.request('POST', endpoint, headers=headers, response = requests.request(
data=json.dumps(payload), timeout=timeout, allow_redirects=True) "POST",
latest_header = json.loads(response.content) endpoint,
if 'error' in latest_header: headers = headers,
pytest.skip(f"Error in hmyv2_latestHeader reply: {latest_header['error']}", allow_module_level=True) data = json.dumps( payload ),
timeout = timeout,
allow_redirects = True,
)
latest_header = json.loads( response.content )
if "error" in latest_header:
pytest.skip(
f"Error in hmyv2_latestHeader reply: {latest_header['error']}",
allow_module_level = True,
)
except Exception as e: except Exception as e:
pytest.skip('Failed to get hmyv2_latestHeader reply', allow_module_level=True) pytest.skip(
"Failed to get hmyv2_latestHeader reply",
allow_module_level = True
)
if metadata and latest_header: if metadata and latest_header:
staking_epoch = metadata['result']['chain-config']['staking-epoch'] staking_epoch = metadata[ "result" ][ "chain-config" ][ "staking-epoch"
current_epoch = latest_header['result']['epoch'] ]
current_epoch = latest_header[ "result" ][ "epoch" ]
if staking_epoch > current_epoch: if staking_epoch > current_epoch:
pytest.skip(f'Not staking epoch: current {current_epoch}, staking {staking_epoch}', allow_module_level=True) pytest.skip(
f"Not staking epoch: current {current_epoch}, staking {staking_epoch}",
allow_module_level = True,
)
def _send_funding_transaction():
try:
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmyv2_sendRawTransaction',
"params": [transfer_raw_transaction]
}
response = requests.request('POST', endpoint_shard_one, headers=headers,
data=json.dumps(payload), timeout=timeout, allow_redirects=True)
tx = json.loads(response.content)
if 'error' in tx:
pytest.skip(f"Error in hmyv2_sendRawTransaction reply: {tx['error']}", allow_module_level=True)
except Exception as e:
pytest.skip('Failed to get hmyv2_sendRawTransaction reply', allow_module_level=True)
def _check_funding_transaction(): def _send_transaction( raw_tx, endpoint ):
try: try:
payload = { payload = {
"id": "1", "id": "1",
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": 'hmyv2_getTransactionByHash', "method": "hmyv2_sendRawTransaction",
"params": [tx_hash] "params": [ raw_tx ],
} }
response = requests.request('POST', endpoint_shard_one, headers=headers, response = requests.request(
data=json.dumps(payload), timeout=timeout, allow_redirects=True) "POST",
tx_data = json.loads(response.content) endpoint,
return tx_data headers = headers,
data = json.dumps( payload ),
timeout = timeout,
allow_redirects = True,
)
tx = json.loads( response.content )
if "error" in tx:
pytest.skip(
f"Error in hmyv2_sendRawTransaction reply: {tx['error']}",
allow_module_level = True,
)
except Exception as e: except Exception as e:
pytest.skip('Failed to get hmyv2_getTransactionByHash reply', allow_module_level=True) pytest.skip(
"Failed to get hmyv2_sendRawTransaction reply",
allow_module_level = True
)
def _check_contract_transaction(): def _check_transaction( tx_hash, endpoint ):
try: try:
payload = { payload = {
"id": "1", "id": "1",
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": 'hmyv2_getTransactionByHash', "method": "hmyv2_getTransactionByHash",
"params": [contract_tx_hash] "params": [ tx_hash ],
} }
response = requests.request('POST', endpoint, headers=headers, response = requests.request(
data=json.dumps(payload), timeout=timeout, allow_redirects=True) "POST",
tx_data = json.loads(response.content) endpoint,
headers = headers,
data = json.dumps( payload ),
timeout = timeout,
allow_redirects = True,
)
tx_data = json.loads( response.content )
return tx_data return tx_data
except Exception as e: except Exception as e:
pytest.skip('Failed to get hmyv2_getTransactionByHash reply', allow_module_level=True) pytest.skip(
"Failed to get hmyv2_getTransactionByHash reply",
allow_module_level = True
)
def _wait_for_transaction_confirmed( tx_hash, endpoint, timeout = 30 ):
start_time = time.time()
while ( time.time() - start_time ) <= timeout:
tx_data = _check_transaction( tx_hash, endpoint )
if tx_data is not None:
block_hash = tx_data[ "result" ].get( "blockHash", "0x00" )
unique_chars = "".join( set( list( block_hash[ 2 : ] ) ) )
if unique_chars != "0":
return True
time.sleep( random.uniform( 0.2, 0.5 ) )
return False
def _send_contract_transaction():
try:
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmyv2_sendRawTransaction',
"params": [contract_raw_transaction]
}
response = requests.request('POST', endpoint, headers=headers,
data=json.dumps(payload), timeout=timeout, allow_redirects=True)
tx_data = json.loads(response.content)
if 'error' in staking_tx:
pytest.skip(f"Error in hmyv2_sendRawTransaction reply: {tx_data['error']}", allow_module_level=True)
except Exception as e:
pytest.skip('Failed to get hmyv2_sendRawTransaction reply', allow_module_level=True)
def _send_staking_transaction(): def _send_staking_transaction( raw_tx, endpoint = endpoint ):
try: try:
payload = { payload = {
"id": "1", "id": "1",
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": 'hmyv2_sendRawStakingTransaction', "method": "hmyv2_sendRawStakingTransaction",
"params": [create_validator_raw_transaction] "params": [ raw_tx ],
} }
response = requests.request('POST', endpoint, headers=headers, response = requests.request(
data=json.dumps(payload), timeout=timeout, allow_redirects=True) "POST",
staking_tx = json.loads(response.content) endpoint,
if 'error' in staking_tx: headers = headers,
pytest.skip(f"Error in hmyv2_sendRawStakingTransaction reply: {staking_tx['error']}", allow_module_level=True) data = json.dumps( payload ),
timeout = timeout,
allow_redirects = True,
)
staking_tx = json.loads( response.content )
if "error" in staking_tx:
pytest.skip(
f"Error in hmyv2_sendRawStakingTransaction reply: {staking_tx['error']}",
allow_module_level = True,
)
except Exception as e: except Exception as e:
pytest.skip('Failed to get hmyv2_sendRawStakingTransaction reply', allow_module_level=True) pytest.skip(
"Failed to get hmyv2_sendRawStakingTransaction reply",
allow_module_level = True,
)
def _check_staking_transaction():
def _check_staking_transaction( stx_hash, endpoint = endpoint ):
try: try:
payload = { payload = {
"id": "1", "id": "1",
"jsonrpc": "2.0", "jsonrpc": "2.0",
"method": 'hmyv2_getStakingTransactionByHash', "method": "hmyv2_getStakingTransactionByHash",
"params": [staking_tx_hash] "params": [ stx_hash ],
} }
response = requests.request('POST', endpoint, headers=headers, response = requests.request(
data=json.dumps(payload), timeout=timeout, allow_redirects=True) "POST",
stx_data = json.loads(response.content) endpoint,
headers = headers,
data = json.dumps( payload ),
timeout = timeout,
allow_redirects = True,
)
stx_data = json.loads( response.content )
return stx_data return stx_data
except Exception as e: except Exception as e:
pytest.skip('Failed to get hmyv2_getStakingTransactionByHash reply', allow_module_level=True) pytest.skip(
"Failed to get hmyv2_getStakingTransactionByHash reply",
allow_module_level = True,
)
def _wait_for_staking_transaction_confirmed( tx_hash, endpoint, timeout = 30 ):
answer = False
start_time = time.time()
while ( time.time() - start_time ) <= timeout:
tx_data = _check_staking_transaction( tx_hash, endpoint )
if tx_data is not None:
block_hash = tx_data[ "result" ].get( "blockHash", "0x00" )
unique_chars = "".join( set( list( block_hash[ 2 : ] ) ) )
if unique_chars != "0":
answer = True
time.sleep( random.uniform( 0.2, 0.5 ) )
return answer

@ -1,116 +1,155 @@
import pytest import pytest
import requests import requests
from pyhmy import ( from pyhmy import account
account
)
from pyhmy.rpc import ( from pyhmy.rpc import exceptions
exceptions
)
explorer_endpoint = "http://localhost:9700"
explorer_endpoint = 'http://localhost:9599' endpoint_shard_one = "http://localhost:9502"
endpoint_shard_one = 'http://localhost:9501' local_test_address = "one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3"
local_test_address = 'one1zksj3evekayy90xt4psrz8h6j2v3hla4qwz4ur' test_validator_address = local_test_address
test_validator_address = 'one18tvf56zqjkjnak686lwutcp5mqfnvee35xjnhc'
genesis_block_number = 0 genesis_block_number = 0
test_block_number = 1 test_block_number = 1
fake_shard = 'http://example.com' fake_shard = "http://example.com"
def _test_account_rpc(fn, *args, **kwargs): def _test_account_rpc( fn, *args, **kwargs ):
if not callable(fn): if not callable( fn ):
pytest.fail(f'Invalid function: {fn}') pytest.fail( f"Invalid function: {fn}" )
try: try:
response = fn(*args, **kwargs) response = fn( *args, **kwargs )
except Exception as e: except Exception as e:
if isinstance(e, exceptions.RPCError) and 'does not exist/is not available' in str(e): if isinstance( e,
pytest.skip(f'{str(e)}') exceptions.RPCError
pytest.fail(f'Unexpected error: {e.__class__} {e}') ) and "does not exist/is not available" in str( e ):
pytest.skip( f"{str(e)}" )
pytest.fail( f"Unexpected error: {e.__class__} {e}" )
return response return response
@pytest.mark.run(order=1) def test_get_balance( setup_blockchain ):
def test_get_balance(setup_blockchain): balance = _test_account_rpc( account.get_balance, local_test_address )
balance = _test_account_rpc(account.get_balance, local_test_address) assert isinstance( balance, int )
assert isinstance(balance, int)
assert balance > 0 assert balance > 0
@pytest.mark.run(order=2)
def test_get_balance_by_block(setup_blockchain): def test_get_balance_by_block( setup_blockchain ):
balance = _test_account_rpc(account.get_balance_by_block, local_test_address, genesis_block_number) balance = _test_account_rpc(
assert isinstance(balance, int) account.get_balance_by_block,
local_test_address,
genesis_block_number
)
assert isinstance( balance, int )
assert balance > 0 assert balance > 0
@pytest.mark.run(order=3)
def test_get_account_nonce(setup_blockchain): def test_get_account_nonce( setup_blockchain ):
true_nonce = _test_account_rpc(account.get_account_nonce, local_test_address, test_block_number, endpoint=endpoint_shard_one) true_nonce = _test_account_rpc(
assert isinstance(true_nonce, int) account.get_account_nonce,
local_test_address,
@pytest.mark.run(order=4) test_block_number,
def test_get_transaction_history(setup_blockchain): endpoint = endpoint_shard_one,
tx_history = _test_account_rpc(account.get_transaction_history, local_test_address, endpoint=explorer_endpoint) )
assert isinstance(tx_history, list) assert isinstance( true_nonce, int )
assert len(tx_history) >= 0
@pytest.mark.run(order=5) def test_get_transaction_history( setup_blockchain ):
def test_get_staking_transaction_history(setup_blockchain): tx_history = _test_account_rpc(
staking_tx_history = _test_account_rpc(account.get_staking_transaction_history, test_validator_address, endpoint=explorer_endpoint) account.get_transaction_history,
assert isinstance(staking_tx_history, list) local_test_address,
assert len(staking_tx_history) > 0 endpoint = explorer_endpoint
)
@pytest.mark.run(order=6) assert isinstance( tx_history, list )
def test_get_balance_on_all_shards(setup_blockchain): assert len( tx_history ) >= 0
balances = _test_account_rpc(account.get_balance_on_all_shards, local_test_address)
assert isinstance(balances, list)
assert len(balances) == 2 def test_get_staking_transaction_history( setup_blockchain ):
staking_tx_history = _test_account_rpc(
@pytest.mark.run(order=7) account.get_staking_transaction_history,
def test_get_total_balance(setup_blockchain): test_validator_address,
total_balance = _test_account_rpc(account.get_total_balance, local_test_address) endpoint = explorer_endpoint,
assert isinstance(total_balance, int) )
assert isinstance( staking_tx_history, list )
assert len( staking_tx_history ) > 0
def test_get_balance_on_all_shards( setup_blockchain ):
balances = _test_account_rpc(
account.get_balance_on_all_shards,
local_test_address
)
assert isinstance( balances, list )
assert len( balances ) == 2
def test_get_total_balance( setup_blockchain ):
total_balance = _test_account_rpc(
account.get_total_balance,
local_test_address
)
assert isinstance( total_balance, int )
assert total_balance > 0 assert total_balance > 0
@pytest.mark.run(order=0)
def test_is_valid_address():
assert account.is_valid_address('one1zksj3evekayy90xt4psrz8h6j2v3hla4qwz4ur')
assert not account.is_valid_address('one1wje75aedczmj4dwjs0812xcg7vx0dy231cajk0')
@pytest.mark.run(order=8) def test_is_valid_address():
def test_get_transaction_count(setup_blockchain): assert account.is_valid_address(
tx_count = _test_account_rpc(account.get_transaction_count, local_test_address, 'latest') "one1zksj3evekayy90xt4psrz8h6j2v3hla4qwz4ur"
assert isinstance(tx_count, int) )
assert not account.is_valid_address(
"one1wje75aedczmj4dwjs0812xcg7vx0dy231cajk0"
)
def test_get_transaction_count( setup_blockchain ):
tx_count = _test_account_rpc(
account.get_transaction_count,
local_test_address,
"latest",
explorer_endpoint
)
assert isinstance( tx_count, int )
assert tx_count > 0 assert tx_count > 0
@pytest.mark.run(order=9)
def test_get_transactions_count(setup_blockchain):
tx_count = _test_account_rpc(account.get_transactions_count, local_test_address, 'ALL')
@pytest.mark.run(order=10) def test_get_transactions_count( setup_blockchain ):
def test_get_staking_transactions_count(setup_blockchain): tx_count = _test_account_rpc(
tx_count = _test_account_rpc(account.get_staking_transactions_count, local_test_address, 'ALL') account.get_transactions_count,
assert isinstance(tx_count, int) local_test_address,
"ALL",
explorer_endpoint
)
def test_get_staking_transactions_count( setup_blockchain ):
tx_count = _test_account_rpc(
account.get_staking_transactions_count,
local_test_address,
"ALL",
explorer_endpoint,
)
assert isinstance( tx_count, int )
@pytest.mark.run(order=10)
def test_errors(): def test_errors():
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_balance('', fake_shard) account.get_balance( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_balance_by_block('', 1, fake_shard) account.get_balance_by_block( "", 1, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_account_nonce('', 1, fake_shard) account.get_account_nonce( "", 1, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_transaction_count('', 1, fake_shard) account.get_transaction_count( "", 1, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_transactions_count('', 1, fake_shard) account.get_transactions_count( "", 1, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_transactions_count('', 'ALL', fake_shard) account.get_transactions_count( "", "ALL", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_transaction_history('', endpoint=fake_shard) account.get_transaction_history( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_staking_transaction_history('', endpoint=fake_shard) account.get_staking_transaction_history( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_balance_on_all_shards('', endpoint=fake_shard) account.get_balance_on_all_shards( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
account.get_total_balance('', endpoint=fake_shard) account.get_total_balance( "", endpoint = fake_shard )

@ -1,321 +1,369 @@
import pytest import pytest
import requests import requests
from pyhmy import ( from pyhmy import blockchain
blockchain
)
from pyhmy.rpc import (
exceptions
)
from pyhmy.rpc import exceptions
test_epoch_number = 0 test_epoch_number = 0
genesis_block_number = 0 genesis_block_number = 0
test_block_number = 1 test_block_number = 1
test_block_hash = None test_block_hash = None
fake_shard = 'http://example.com' fake_shard = "http://example.com"
address = "one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3"
def _test_blockchain_rpc(fn, *args, **kwargs): def _test_blockchain_rpc( fn, *args, **kwargs ):
if not callable(fn): if not callable( fn ):
pytest.fail(f'Invalid function: {fn}') pytest.fail( f"Invalid function: {fn}" )
try: try:
response = fn(*args, **kwargs) response = fn( *args, **kwargs )
except Exception as e: except Exception as e:
if isinstance(e, exceptions.RPCError) and 'does not exist/is not available' in str(e): if isinstance( e,
pytest.skip(f'{str(e)}') exceptions.RPCError
pytest.fail(f'Unexpected error: {e.__class__} {e}') ) and "does not exist/is not available" in str( e ):
pytest.skip( f"{str(e)}" )
pytest.fail( f"Unexpected error: {e.__class__} {e}" )
return response return response
@pytest.mark.run(order=1)
def test_get_node_metadata(setup_blockchain): def test_get_node_metadata( setup_blockchain ):
metadata = _test_blockchain_rpc(blockchain.get_node_metadata) metadata = _test_blockchain_rpc( blockchain.get_node_metadata )
assert isinstance(metadata, dict) assert isinstance( metadata, dict )
@pytest.mark.run(order=2)
def test_get_sharding_structure(setup_blockchain): def test_get_sharding_structure( setup_blockchain ):
sharding_structure = _test_blockchain_rpc(blockchain.get_sharding_structure) sharding_structure = _test_blockchain_rpc(
assert isinstance(sharding_structure, list) blockchain.get_sharding_structure
assert len(sharding_structure) > 0 )
assert isinstance( sharding_structure, list )
@pytest.mark.run(order=3) assert len( sharding_structure ) > 0
def test_get_leader_address(setup_blockchain):
leader = _test_blockchain_rpc(blockchain.get_leader_address)
assert isinstance(leader, str) def test_get_leader_address( setup_blockchain ):
assert 'one1' in leader leader = _test_blockchain_rpc( blockchain.get_leader_address )
assert isinstance( leader, str )
@pytest.mark.run(order=4) assert "one1" in leader
def test_get_block_number(setup_blockchain):
current_block_number = _test_blockchain_rpc(blockchain.get_block_number)
assert isinstance(current_block_number, int) def test_get_block_number( setup_blockchain ):
current_block_number = _test_blockchain_rpc( blockchain.get_block_number )
@pytest.mark.run(order=5) assert isinstance( current_block_number, int )
def test_get_current_epoch(setup_blockchain):
current_epoch = _test_blockchain_rpc(blockchain.get_current_epoch)
assert isinstance(current_epoch, int) def test_get_current_epoch( setup_blockchain ):
current_epoch = _test_blockchain_rpc( blockchain.get_current_epoch )
@pytest.mark.run(order=6) assert isinstance( current_epoch, int )
def tset_get_gas_price(setup_blockchain):
gas = _test_blockchain_rpc(blockchain.get_gas_price)
assert isinstance(gas, int) def tset_get_gas_price( setup_blockchain ):
gas = _test_blockchain_rpc( blockchain.get_gas_price )
@pytest.mark.run(order=7) assert isinstance( gas, int )
def test_get_num_peers(setup_blockchain):
peers = _test_blockchain_rpc(blockchain.get_num_peers)
assert isinstance(peers, int) def test_get_num_peers( setup_blockchain ):
peers = _test_blockchain_rpc( blockchain.get_num_peers )
@pytest.mark.run(order=8) assert isinstance( peers, int )
def test_get_latest_header(setup_blockchain):
header = _test_blockchain_rpc(blockchain.get_latest_header)
assert isinstance(header, dict) def test_get_latest_header( setup_blockchain ):
header = _test_blockchain_rpc( blockchain.get_latest_header )
@pytest.mark.run(order=9) assert isinstance( header, dict )
def test_get_latest_chain_headers(setup_blockchain):
header_pair = _test_blockchain_rpc(blockchain.get_latest_chain_headers)
assert isinstance(header_pair, dict) def test_get_latest_chain_headers( setup_blockchain ):
header_pair = _test_blockchain_rpc( blockchain.get_latest_chain_headers )
@pytest.mark.run(order=10) assert isinstance( header_pair, dict )
def test_get_block_by_number(setup_blockchain):
def test_get_block_by_number( setup_blockchain ):
global test_block_hash global test_block_hash
block = _test_blockchain_rpc(blockchain.get_block_by_number, test_block_number) block = _test_blockchain_rpc(
assert isinstance(block, dict) blockchain.get_block_by_number,
assert 'hash' in block.keys() test_block_number
test_block_hash = block['hash'] )
assert isinstance( block, dict )
assert "hash" in block.keys()
test_block_hash = block[ "hash" ]
@pytest.mark.run(order=11)
def test_get_block_by_hash(setup_blockchain): def test_get_block_by_hash( setup_blockchain ):
if not test_block_hash: if not test_block_hash:
pytest.skip('Failed to get reference block hash') pytest.skip( "Failed to get reference block hash" )
block = _test_blockchain_rpc(blockchain.get_block_by_hash, test_block_hash) block = _test_blockchain_rpc(
assert isinstance(block, dict) blockchain.get_block_by_hash,
test_block_hash
)
assert isinstance( block, dict )
@pytest.mark.run(order=12) def test_get_block_transaction_count_by_number( setup_blockchain ):
def test_get_block_transaction_count_by_number(setup_blockchain): tx_count = _test_blockchain_rpc(
tx_count = _test_blockchain_rpc(blockchain.get_block_transaction_count_by_number, test_block_number) blockchain.get_block_transaction_count_by_number,
assert isinstance(tx_count, int) test_block_number
)
assert isinstance( tx_count, int )
@pytest.mark.run(order=13)
def test_get_block_transaction_count_by_hash(setup_blockchain): def test_get_block_transaction_count_by_hash( setup_blockchain ):
if not test_block_hash: if not test_block_hash:
pytest.skip('Failed to get reference block hash') pytest.skip( "Failed to get reference block hash" )
tx_count = _test_blockchain_rpc(blockchain.get_block_transaction_count_by_hash, test_block_hash) tx_count = _test_blockchain_rpc(
assert isinstance(tx_count, int) blockchain.get_block_transaction_count_by_hash,
test_block_hash
@pytest.mark.run(order=14) )
def test_get_blocks(setup_blockchain): assert isinstance( tx_count, int )
blocks = _test_blockchain_rpc(blockchain.get_blocks, genesis_block_number, test_block_number)
assert isinstance(blocks, list)
assert len(blocks) == (test_block_number - genesis_block_number + 1) def test_get_blocks( setup_blockchain ):
blocks = _test_blockchain_rpc(
@pytest.mark.run(order=15) blockchain.get_blocks,
def test_get_block_signers(setup_blockchain): genesis_block_number,
block_signers = _test_blockchain_rpc(blockchain.get_block_signers, test_block_number) test_block_number
assert isinstance(block_signers, list) )
assert len(block_signers) > 0 assert isinstance( blocks, list )
assert len( blocks ) == ( test_block_number - genesis_block_number + 1 )
@pytest.mark.run(order=16)
def test_get_validators(setup_blockchain):
validators = _test_blockchain_rpc(blockchain.get_validators, test_epoch_number) def test_get_block_signers( setup_blockchain ):
assert isinstance(validators, dict) block_signers = _test_blockchain_rpc(
assert 'validators' in validators.keys() blockchain.get_block_signers,
assert len(validators['validators']) > 0 test_block_number
)
@pytest.mark.run(order=17) assert isinstance( block_signers, list )
def test_get_shard(setup_blockchain): assert len( block_signers ) > 0
shard = _test_blockchain_rpc(blockchain.get_shard)
assert isinstance(shard, int)
def test_get_validators( setup_blockchain ):
validators = _test_blockchain_rpc(
blockchain.get_validators,
test_epoch_number
)
assert isinstance( validators, dict )
assert "validators" in validators.keys()
assert len( validators[ "validators" ] ) > 0
def test_get_shard( setup_blockchain ):
shard = _test_blockchain_rpc( blockchain.get_shard )
assert isinstance( shard, int )
assert shard == 0 assert shard == 0
@pytest.mark.run(order=18)
def test_get_staking_epoch(setup_blockchain):
staking_epoch = _test_blockchain_rpc(blockchain.get_staking_epoch)
assert isinstance(staking_epoch, int)
@pytest.mark.run(order=19) def test_get_staking_epoch( setup_blockchain ):
def test_get_prestaking_epoch(setup_blockchain): staking_epoch = _test_blockchain_rpc( blockchain.get_staking_epoch )
prestaking_epoch = _test_blockchain_rpc(blockchain.get_prestaking_epoch) assert isinstance( staking_epoch, int )
assert isinstance(prestaking_epoch, int)
@pytest.mark.run(order=20) def test_get_prestaking_epoch( setup_blockchain ):
def test_get_bad_blocks(setup_blockchain): prestaking_epoch = _test_blockchain_rpc( blockchain.get_prestaking_epoch )
assert isinstance( prestaking_epoch, int )
def test_get_bad_blocks( setup_blockchain ):
# TODO: Remove skip when RPC is fixed # TODO: Remove skip when RPC is fixed
pytest.skip("Known error with hmyv2_getCurrentBadBlocks") pytest.skip( "Known error with hmyv2_getCurrentBadBlocks" )
bad_blocks = _test_blockchain_rpc(blockchain.get_bad_blocks) bad_blocks = _test_blockchain_rpc( blockchain.get_bad_blocks )
assert isinstance(bad_blocks, list) assert isinstance( bad_blocks, list )
@pytest.mark.run(order=21)
def test_get_validator_keys(setup_blockchain): def test_get_validator_keys( setup_blockchain ):
keys = _test_blockchain_rpc(blockchain.get_validator_keys, test_epoch_number) keys = _test_blockchain_rpc(
assert isinstance(keys, list) blockchain.get_validator_keys,
assert len(keys) > 0 test_epoch_number
)
@pytest.mark.run(order=22) assert isinstance( keys, list )
def test_get_block_signers_keys(setup_blockchain): assert len( keys ) > 0
keys = _test_blockchain_rpc(blockchain.get_block_signers_keys, test_block_number)
assert isinstance(keys, list)
assert len(keys) > 0 def test_get_block_signers_keys( setup_blockchain ):
keys = _test_blockchain_rpc(
@pytest.mark.run(order=23) blockchain.get_block_signers_keys,
def test_chain_id(setup_blockchain): test_block_number
chain_id = _test_blockchain_rpc(blockchain.chain_id) )
assert isinstance(chain_id, int) assert isinstance( keys, list )
assert len( keys ) > 0
@pytest.mark.run(order=24)
def test_get_peer_info(setup_blockchain):
peer_info = _test_blockchain_rpc(blockchain.get_peer_info) def test_chain_id( setup_blockchain ):
assert isinstance(peer_info, dict) chain_id = _test_blockchain_rpc( blockchain.chain_id )
assert isinstance( chain_id, int )
@pytest.mark.run(order=25)
def test_protocol_version(setup_blockchain):
protocol_version = _test_blockchain_rpc(blockchain.protocol_version) def test_get_peer_info( setup_blockchain ):
assert isinstance(protocol_version, int) peer_info = _test_blockchain_rpc( blockchain.get_peer_info )
assert isinstance( peer_info, dict )
@pytest.mark.run(order=26)
def test_is_last_block(setup_blockchain):
is_last_block = _test_blockchain_rpc(blockchain.is_last_block, 0) def test_protocol_version( setup_blockchain ):
assert isinstance(is_last_block, bool) protocol_version = _test_blockchain_rpc( blockchain.protocol_version )
assert isinstance( protocol_version, int )
def test_is_last_block( setup_blockchain ):
is_last_block = _test_blockchain_rpc( blockchain.is_last_block, 0 )
assert isinstance( is_last_block, bool )
assert not is_last_block assert not is_last_block
@pytest.mark.run(order=27)
def test_epoch_last_block(setup_blockchain): def test_epoch_last_block( setup_blockchain ):
epoch_last_block = _test_blockchain_rpc(blockchain.epoch_last_block, 0) epoch_last_block = _test_blockchain_rpc( blockchain.epoch_last_block, 0 )
assert isinstance(epoch_last_block, int) assert isinstance( epoch_last_block, int )
@pytest.mark.run(order=28)
def test_get_circulating_supply(setup_blockchain): def test_get_circulating_supply( setup_blockchain ):
circulating_supply = _test_blockchain_rpc(blockchain.get_circulating_supply) circulating_supply = _test_blockchain_rpc(
assert isinstance(circulating_supply, str) blockchain.get_circulating_supply
)
@pytest.mark.run(order=29) assert isinstance( circulating_supply, str )
def test_get_total_supply(setup_blockchain):
total_supply = _test_blockchain_rpc(blockchain.get_total_supply)
assert isinstance(total_supply, str) or total_supply == None def test_get_total_supply( setup_blockchain ):
total_supply = _test_blockchain_rpc( blockchain.get_total_supply )
@pytest.mark.run(order=30) assert isinstance( total_supply, str ) or total_supply == None
def test_get_last_cross_links(setup_blockchain):
last_cross_links = _test_blockchain_rpc(blockchain.get_last_cross_links)
assert isinstance(last_cross_links, list) def test_get_last_cross_links( setup_blockchain ):
last_cross_links = _test_blockchain_rpc( blockchain.get_last_cross_links )
@pytest.mark.run(order=31) assert isinstance( last_cross_links, list )
def test_get_gas_price(setup_blockchain):
gas_price = _test_blockchain_rpc(blockchain.get_gas_price)
assert isinstance(gas_price, int) def test_get_gas_price( setup_blockchain ):
gas_price = _test_blockchain_rpc( blockchain.get_gas_price )
@pytest.mark.run(order=32) assert isinstance( gas_price, int )
def test_get_version(setup_blockchain):
version = _test_blockchain_rpc(blockchain.get_version)
assert isinstance(version, int) def test_get_version( setup_blockchain ):
version = _test_blockchain_rpc( blockchain.get_version )
@pytest.mark.run(order=33) assert isinstance( version, int )
def test_get_header_by_number(setup_blockchain):
header_pair = _test_blockchain_rpc(blockchain.get_header_by_number, 0)
assert isinstance(header_pair, dict) def test_get_header_by_number( setup_blockchain ):
header_pair = _test_blockchain_rpc( blockchain.get_header_by_number, 0 )
@pytest.mark.run(order=34) assert isinstance( header_pair, dict )
def test_get_block_staking_transaction_count_by_number(setup_blockchain):
tx_count = _test_blockchain_rpc(blockchain.get_block_staking_transaction_count_by_number, test_block_number)
assert isinstance(tx_count, int) def test_get_block_staking_transaction_count_by_number( setup_blockchain ):
tx_count = _test_blockchain_rpc(
@pytest.mark.run(order=35) blockchain.get_block_staking_transaction_count_by_number,
def test_get_block_staking_transaction_count_by_hash(setup_blockchain): test_block_number
)
assert isinstance( tx_count, int )
def test_get_block_staking_transaction_count_by_hash( setup_blockchain ):
if not test_block_hash: if not test_block_hash:
pytest.skip('Failed to get reference block hash') pytest.skip( "Failed to get reference block hash" )
tx_count = _test_blockchain_rpc(blockchain.get_block_staking_transaction_count_by_hash, test_block_hash) tx_count = _test_blockchain_rpc(
assert isinstance(tx_count, int) blockchain.get_block_staking_transaction_count_by_hash,
test_block_hash
@pytest.mark.run(order=36) )
def test_is_block_signer(setup_blockchain): assert isinstance( tx_count, int )
is_signer = _test_blockchain_rpc(blockchain.is_block_signer, test_block_number, '0x0')
assert isinstance(is_signer, bool)
def test_is_block_signer( setup_blockchain ):
@pytest.mark.run(order=37) is_signer = _test_blockchain_rpc(
def test_get_signed_blocks(setup_blockchain): blockchain.is_block_signer,
signed_blocks = _test_blockchain_rpc(blockchain.get_signed_blocks, '0x0') test_block_number,
assert isinstance(signed_blocks, int) address
)
@pytest.mark.run(order=38) assert isinstance( is_signer, bool )
def test_in_sync(setup_blockchain):
in_sync = _test_blockchain_rpc(blockchain.in_sync)
assert isinstance(in_sync, bool) def test_get_signed_blocks( setup_blockchain ):
signed_blocks = _test_blockchain_rpc(
@pytest.mark.run(order=38) blockchain.get_signed_blocks,
def test_beacon_in_sync(setup_blockchain): address
beacon_in_sync = _test_blockchain_rpc(blockchain.beacon_in_sync) )
assert isinstance(beacon_in_sync, bool) assert isinstance( signed_blocks, int )
def test_in_sync( setup_blockchain ):
in_sync = _test_blockchain_rpc( blockchain.in_sync )
assert isinstance( in_sync, bool )
def test_beacon_in_sync( setup_blockchain ):
beacon_in_sync = _test_blockchain_rpc( blockchain.beacon_in_sync )
assert isinstance( beacon_in_sync, bool )
def test_errors(): def test_errors():
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.chain_id(fake_shard) blockchain.chain_id( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_node_metadata(fake_shard) blockchain.get_node_metadata( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_peer_info(fake_shard) blockchain.get_peer_info( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.protocol_version(fake_shard) blockchain.protocol_version( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_shard(fake_shard) blockchain.get_shard( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_staking_epoch(fake_shard) blockchain.get_staking_epoch( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_prestaking_epoch(fake_shard) blockchain.get_prestaking_epoch( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_sharding_structure(fake_shard) blockchain.get_sharding_structure( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_leader_address(fake_shard) blockchain.get_leader_address( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.is_last_block(0, fake_shard) blockchain.is_last_block( 0, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.epoch_last_block(0, fake_shard) blockchain.epoch_last_block( 0, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_circulating_supply(fake_shard) blockchain.get_circulating_supply( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_total_supply(fake_shard) blockchain.get_total_supply( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_block_number(fake_shard) blockchain.get_block_number( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_current_epoch(fake_shard) blockchain.get_current_epoch( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_last_cross_links(fake_shard) blockchain.get_last_cross_links( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_gas_price(fake_shard) blockchain.get_gas_price( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_num_peers(fake_shard) blockchain.get_num_peers( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_version(fake_shard) blockchain.get_version( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_latest_header(fake_shard) blockchain.get_latest_header( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_header_by_number(0, fake_shard) blockchain.get_header_by_number( 0, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_latest_chain_headers(fake_shard) blockchain.get_latest_chain_headers( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_block_by_number(0, endpoint=fake_shard) blockchain.get_block_by_number( 0, endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_block_by_hash('', endpoint=fake_shard) blockchain.get_block_by_hash( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_block_transaction_count_by_number(0, fake_shard) blockchain.get_block_transaction_count_by_number( 0, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_block_transaction_count_by_hash('', fake_shard) blockchain.get_block_transaction_count_by_hash( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
blockchain.get_block_staking_transaction_count_by_number(0, fake_shard) blockchain.get_block_staking_transaction_count_by_number(
with pytest.raises(exceptions.RPCError): 0,
blockchain.get_block_staking_transaction_count_by_hash('', fake_shard) fake_shard
with pytest.raises(exceptions.RPCError): )
blockchain.get_blocks(0, 1, endpoint=fake_shard) with pytest.raises( exceptions.RPCError ):
with pytest.raises(exceptions.RPCError): blockchain.get_block_staking_transaction_count_by_hash( "", fake_shard )
blockchain.get_block_signers(0, fake_shard) with pytest.raises( exceptions.RPCError ):
with pytest.raises(exceptions.RPCError): blockchain.get_blocks( 0, 1, endpoint = fake_shard )
blockchain.get_block_signers_keys(0, fake_shard) with pytest.raises( exceptions.RPCError ):
with pytest.raises(exceptions.RPCError): blockchain.get_block_signers( 0, fake_shard )
blockchain.is_block_signer(0, '', fake_shard) with pytest.raises( exceptions.RPCError ):
with pytest.raises(exceptions.RPCError): blockchain.get_block_signers_keys( 0, fake_shard )
blockchain.get_signed_blocks('', fake_shard) with pytest.raises( exceptions.RPCError ):
with pytest.raises(exceptions.RPCError): blockchain.is_block_signer( 0, "", fake_shard )
blockchain.get_validators(1, fake_shard) with pytest.raises( exceptions.RPCError ):
with pytest.raises(exceptions.RPCError): blockchain.get_signed_blocks( "", fake_shard )
blockchain.get_validator_keys(0, fake_shard) with pytest.raises( exceptions.RPCError ):
with pytest.raises(exceptions.RPCError): blockchain.get_validators( 1, fake_shard )
blockchain.in_sync(fake_shard) with pytest.raises( exceptions.RPCError ):
with pytest.raises(exceptions.RPCError): blockchain.get_validator_keys( 0, fake_shard )
blockchain.beacon_in_sync(fake_shard) with pytest.raises( exceptions.RPCError ):
blockchain.in_sync( fake_shard )
with pytest.raises( exceptions.RPCError ):
blockchain.beacon_in_sync( fake_shard )

@ -1,74 +1,86 @@
import pytest import pytest
from pyhmy import ( from pyhmy import contract
contract
)
from pyhmy.rpc import ( from pyhmy.rpc import exceptions
exceptions
)
explorer_endpoint = 'http://localhost:9599' explorer_endpoint = "http://localhost:9599"
contract_tx_hash = '0xa13414dd152173395c69a11e79dea31bf029660f747a42a53744181d05571e70' contract_tx_hash = "0xa605852dd2fa39ed42e101c17aaca9d344d352ba9b24b14b9af94ec9cb58b31f"
# deployedBytecode from json file
contract_code = "0x6080604052348015600f57600080fd5b506004361060285760003560e01c80634936cd3614602d575b600080fd5b604080516001815290519081900360200190f3fea2646970667358221220fa3fa0e8d0267831a59f4dd5edf39a513d07e98461cb06660ad28d4beda744cd64736f6c634300080f0033"
contract_address = None contract_address = None
fake_shard = 'http://example.com' fake_shard = "http://example.com"
def _test_contract_rpc(fn, *args, **kwargs):
if not callable(fn): def _test_contract_rpc( fn, *args, **kwargs ):
pytest.fail(f'Invalid function: {fn}') if not callable( fn ):
pytest.fail( f"Invalid function: {fn}" )
try: try:
response = fn(*args, **kwargs) response = fn( *args, **kwargs )
except Exception as e: except Exception as e:
if isinstance(e, exceptions.RPCError) and 'does not exist/is not available' in str(e): if isinstance( e,
pytest.skip(f'{str(e)}') exceptions.RPCError
elif isinstance(e, exceptions.RPCError) and 'estimateGas returned' in str(e): ) and "does not exist/is not available" in str( e ):
pytest.skip(f'{str(e)}') pytest.skip( f"{str(e)}" )
pytest.fail(f'Unexpected error: {e.__class__} {e}') elif isinstance( e,
exceptions.RPCError
) and "estimateGas returned" in str( e ):
pytest.skip( f"{str(e)}" )
pytest.fail( f"Unexpected error: {e.__class__} {e}" )
return response return response
@pytest.mark.run(order=1)
def test_get_contract_address_from_hash(setup_blockchain): def test_get_contract_address_from_hash( setup_blockchain ):
global contract_address global contract_address
contract_address = _test_contract_rpc(contract.get_contract_address_from_hash, contract_tx_hash) contract_address = _test_contract_rpc(
assert isinstance(contract_address, str) contract.get_contract_address_from_hash,
contract_tx_hash
)
assert isinstance( contract_address, str )
@pytest.mark.run(order=2)
def test_call(setup_blockchain): def test_call( setup_blockchain ):
if not contract_address: if not contract_address:
pytest.skip('Contract address not loaded yet') pytest.skip( "Contract address not loaded yet" )
called = _test_contract_rpc(contract.call, contract_address, 'latest') called = _test_contract_rpc( contract.call, contract_address, "latest" )
assert isinstance(called, str) and called.startswith('0x') assert isinstance( called, str ) and called.startswith( "0x" )
@pytest.mark.run(order=3) def test_estimate_gas( setup_blockchain ):
def test_estimate_gas(setup_blockchain):
if not contract_address: if not contract_address:
pytest.skip('Contract address not loaded yet') pytest.skip( "Contract address not loaded yet" )
gas = _test_contract_rpc(contract.estimate_gas, contract_address) gas = _test_contract_rpc( contract.estimate_gas, contract_address )
assert isinstance(gas, int) assert isinstance( gas, int )
@pytest.mark.run(order=4)
def test_get_code(setup_blockchain): def test_get_code( setup_blockchain ):
if not contract_address: if not contract_address:
pytest.skip('Contract address not loaded yet') pytest.skip( "Contract address not loaded yet" )
code = _test_contract_rpc(contract.get_code, contract_address, 'latest') code = _test_contract_rpc( contract.get_code, contract_address, "latest" )
assert code == '0x608060405234801561001057600080fd5b50600436106100415760003560e01c8063445df0ac146100465780638da5cb5b14610064578063fdacd576146100ae575b600080fd5b61004e6100dc565b6040518082815260200191505060405180910390f35b61006c6100e2565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100da600480360360208110156100c457600080fd5b8101908080359060200190929190505050610107565b005b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561016457806001819055505b5056fea265627a7a723158209b80813a158b44af65aee232b44c0ac06472c48f4abbe298852a39f0ff34a9f264736f6c63430005100032' assert code == contract_code
@pytest.mark.run(order=5) def test_get_storage_at( setup_blockchain ):
def test_get_storage_at(setup_blockchain):
if not contract_address: if not contract_address:
pytest.skip('Contract address not loaded yet') pytest.skip( "Contract address not loaded yet" )
storage = _test_contract_rpc(contract.get_storage_at, contract_address, '0x0', 'latest') storage = _test_contract_rpc(
assert isinstance(storage, str) and storage.startswith('0x') contract.get_storage_at,
contract_address,
"0x0",
"latest"
)
assert isinstance( storage, str ) and storage.startswith( "0x" )
def test_errors(): def test_errors():
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
contract.get_contract_address_from_hash('', fake_shard) contract.get_contract_address_from_hash( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
contract.call('', '', endpoint=fake_shard) contract.call( "", "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
contract.estimate_gas('', endpoint=fake_shard) contract.estimate_gas( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
contract.get_code('', 'latest', endpoint=fake_shard) contract.get_code( "", "latest", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
contract.get_storage_at('', 1, 'latest', endpoint=fake_shard) contract.get_storage_at( "", 1, "latest", endpoint = fake_shard )

@ -1,7 +1,4 @@
from pyhmy import ( from pyhmy import signing
signing
)
""" """
Test signature source (node.js) Test signature source (node.js)
import { Transaction, RLPSign, TxStatus } from '@harmony-js/transaction'; import { Transaction, RLPSign, TxStatus } from '@harmony-js/transaction';
@ -30,16 +27,25 @@ let signed = RLPSign(transaction, privateKey);
console.log( 'Signed transaction' ) console.log( 'Signed transaction' )
console.log(signed) console.log(signed)
""" """
def test_eth_transaction(): def test_eth_transaction():
transaction_dict = { transaction_dict = {
'nonce': 2, "nonce": 2,
'gasPrice': 1, "gasPrice": 1,
'gas': 100, # signing.py uses Ether, which by default calls it gas "gas": 100, # signing.py uses Ether, which by default calls it gas
'to': '0x14791697260e4c9a71f18484c9f997b308e59325', "to": "0x14791697260e4c9a71f18484c9f997b308e59325",
'value': 5, "value": 5,
} }
signed_tx = signing.sign_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') signed_tx = signing.sign_transaction(
assert signed_tx.rawTransaction.hex() == '0xf85d0201649414791697260e4c9a71f18484c9f997b308e5932505801ca0b364f4296bfd3231889d1b9ac94c68abbcb8ee6a6c7a5fa412ac82b5b7b0d5d1a02233864842ab28ee4f99c207940a867b0f8534ca362836190792816b48dde3b1' transaction_dict,
"4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48",
)
assert (
signed_tx.rawTransaction.hex() ==
"0xf85d0201649414791697260e4c9a71f18484c9f997b308e5932505801ca0b364f4296bfd3231889d1b9ac94c68abbcb8ee6a6c7a5fa412ac82b5b7b0d5d1a02233864842ab28ee4f99c207940a867b0f8534ca362836190792816b48dde3b1"
)
""" """
Test signature source (node.js) Test signature source (node.js)
@ -71,16 +77,24 @@ let signed = RLPSign(transaction, privateKey);
console.log( 'Signed transaction' ) console.log( 'Signed transaction' )
console.log(signed) console.log(signed)
""" """
def test_hmy_transaction(): def test_hmy_transaction():
transaction_dict = { transaction_dict = {
'nonce': 2, "nonce": 2,
'gasPrice': 1, "gasPrice": 1,
'gas': 100, # signing.py uses Ether, which by default calls it gas "gas": 100, # signing.py uses Ether, which by default calls it gas
'to': '0x14791697260e4c9a71f18484c9f997b308e59325', "to": "0x14791697260e4c9a71f18484c9f997b308e59325",
'value': 5, "value": 5,
'shardID': 0, "shardID": 0,
'toShardID': 1, "toShardID": 1,
'chainId': 'HmyMainnet' "chainId": "HmyMainnet",
} }
signed_tx = signing.sign_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') signed_tx = signing.sign_transaction(
assert signed_tx.rawTransaction.hex() == '0xf85f02016480019414791697260e4c9a71f18484c9f997b308e59325058026a02a203357ca6d7cdec981ad3d3692ad2c9e24536a9b6e7b486ce2f94f28c7563ea010d38cd0312a153af0aa7d8cd986040c36118bba373cb94e3e86fd4aedce904d' transaction_dict,
"4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48",
)
assert (
signed_tx.rawTransaction.hex() ==
"0xf85f02016480019414791697260e4c9a71f18484c9f997b308e59325058026a02a203357ca6d7cdec981ad3d3692ad2c9e24536a9b6e7b486ce2f94f28c7563ea010d38cd0312a153af0aa7d8cd986040c36118bba373cb94e3e86fd4aedce904d"
)

@ -1,191 +1,248 @@
import pytest import pytest
import requests import requests
from pyhmy import ( from pyhmy import staking
staking
)
from pyhmy.rpc import ( from pyhmy.rpc import exceptions
exceptions
)
explorer_endpoint = "http://localhost:9700"
test_validator_address = "one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3"
fake_shard = "http://example.com"
explorer_endpoint = 'http://localhost:9599'
test_validator_address = 'one18tvf56zqjkjnak686lwutcp5mqfnvee35xjnhc'
fake_shard = 'http://example.com'
def _test_staking_rpc(fn, *args, **kwargs): def _test_staking_rpc( fn, *args, **kwargs ):
if not callable(fn): if not callable( fn ):
pytest.fail(f'Invalid function: {fn}') pytest.fail( f"Invalid function: {fn}" )
try: try:
response = fn(*args, **kwargs) response = fn( *args, **kwargs )
except Exception as e: except Exception as e:
if isinstance(e, exceptions.RPCError) and 'does not exist/is not available' in str(e): if isinstance( e,
pytest.skip(f'{str(e)}') exceptions.RPCError
pytest.fail(f'Unexpected error: {e.__class__} {e}') ) and "does not exist/is not available" in str( e ):
pytest.skip( f"{str(e)}" )
pytest.fail( f"Unexpected error: {e.__class__} {e}" )
return response return response
@pytest.mark.run(order=1)
def test_get_all_validator_addresses(setup_blockchain): def test_get_all_validator_addresses( setup_blockchain ):
validator_addresses = _test_staking_rpc(staking.get_all_validator_addresses) validator_addresses = _test_staking_rpc(
assert isinstance(validator_addresses, list) staking.get_all_validator_addresses
assert len(validator_addresses) > 0 )
assert isinstance( validator_addresses, list )
assert len( validator_addresses ) > 0
assert test_validator_address in validator_addresses assert test_validator_address in validator_addresses
@pytest.mark.run(order=2)
def test_get_validator_information(setup_blockchain): def test_get_validator_information( setup_blockchain ):
info = _test_staking_rpc(staking.get_validator_information, test_validator_address) info = _test_staking_rpc(
assert isinstance(info, dict) staking.get_validator_information,
test_validator_address
@pytest.mark.run(order=3) )
def test_get_all_validator_information(setup_blockchain): assert isinstance( info, dict )
all_validator_information = _test_staking_rpc(staking.get_all_validator_information)
assert isinstance(all_validator_information, list)
assert len(all_validator_information) > 0 def test_get_all_validator_information( setup_blockchain ):
all_validator_information = _test_staking_rpc(
@pytest.mark.run(order=4) staking.get_all_validator_information
def test_get_delegations_by_delegator(setup_blockchain): )
delegations = _test_staking_rpc(staking.get_delegations_by_delegator, test_validator_address) assert isinstance( all_validator_information, list )
assert isinstance(delegations, list) assert len( all_validator_information ) > 0
assert len(delegations) > 0
@pytest.mark.run(order=5) def test_get_delegations_by_delegator( setup_blockchain ):
def test_get_delegations_by_validator(setup_blockchain): delegations = _test_staking_rpc(
delegations = _test_staking_rpc(staking.get_delegations_by_validator, test_validator_address) staking.get_delegations_by_delegator,
assert isinstance(delegations, list) test_validator_address
assert len(delegations) > 0 )
assert isinstance( delegations, list )
@pytest.mark.run(order=6) assert len( delegations ) > 0
def test_get_current_utility_metrics(setup_blockchain):
metrics = _test_staking_rpc(staking.get_current_utility_metrics)
assert isinstance(metrics, dict) def test_get_delegations_by_validator( setup_blockchain ):
delegations = _test_staking_rpc(
@pytest.mark.run(order=7) staking.get_delegations_by_validator,
def test_get_staking_network_info(setup_blockchain): test_validator_address
info = _test_staking_rpc(staking.get_staking_network_info) )
assert isinstance(info, dict) assert isinstance( delegations, list )
assert len( delegations ) > 0
@pytest.mark.run(order=8)
def test_get_super_committees(setup_blockchain):
committee = _test_staking_rpc(staking.get_super_committees) def test_get_current_utility_metrics( setup_blockchain ):
assert isinstance(committee, dict) metrics = _test_staking_rpc( staking.get_current_utility_metrics )
assert isinstance( metrics, dict )
@pytest.mark.run(order=9)
def test_get_raw_median_stake_snapshot(setup_blockchain):
median_stake = _test_staking_rpc(staking.get_raw_median_stake_snapshot) def test_get_staking_network_info( setup_blockchain ):
assert isinstance(median_stake, dict) info = _test_staking_rpc( staking.get_staking_network_info )
assert isinstance( info, dict )
@pytest.mark.run(order=10)
def test_get_validator_information_by_block(setup_blockchain):
def test_get_super_committees( setup_blockchain ):
committee = _test_staking_rpc( staking.get_super_committees )
assert isinstance( committee, dict )
def test_get_raw_median_stake_snapshot( setup_blockchain ):
median_stake = _test_staking_rpc( staking.get_raw_median_stake_snapshot )
assert isinstance( median_stake, dict )
def test_get_validator_information_by_block( setup_blockchain ):
# Apparently validator information not created until block after create-validator transaction is accepted, so +1 block # Apparently validator information not created until block after create-validator transaction is accepted, so +1 block
info = _test_staking_rpc(staking.get_validator_information_by_block_number, test_validator_address, setup_blockchain + 1, endpoint=explorer_endpoint) info = _test_staking_rpc(
assert isinstance(info, dict) staking.get_validator_information_by_block_number,
test_validator_address,
"latest",
endpoint = explorer_endpoint,
)
assert isinstance( info, dict )
@pytest.mark.run(order=11) def test_get_validator_information_by_block( setup_blockchain ):
def test_get_validator_information_by_block(setup_blockchain):
# Apparently validator information not created until block after create-validator transaction is accepted, so +1 block # Apparently validator information not created until block after create-validator transaction is accepted, so +1 block
info = _test_staking_rpc(staking.get_all_validator_information_by_block_number, setup_blockchain + 1, endpoint=explorer_endpoint) info = _test_staking_rpc(
assert isinstance(info, list) staking.get_all_validator_information_by_block_number,
"latest",
@pytest.mark.run(order=12) endpoint = explorer_endpoint,
def test_get_delegations_by_delegator_by_block(setup_blockchain): )
delegations = _test_staking_rpc(staking.get_delegations_by_delegator_by_block_number, test_validator_address, setup_blockchain + 1, endpoint=explorer_endpoint) assert isinstance( info, list )
assert isinstance(delegations, list)
@pytest.mark.run(order=13) def test_get_delegations_by_delegator_by_block( setup_blockchain ):
def test_get_elected_validator_addresses(setup_blockchain): delegations = _test_staking_rpc(
validator_addresses = _test_staking_rpc(staking.get_elected_validator_addresses) staking.get_delegations_by_delegator_by_block_number,
assert isinstance(validator_addresses, list) test_validator_address,
assert len(validator_addresses) > 0 "latest",
endpoint = explorer_endpoint,
@pytest.mark.run(order=14) )
def test_get_validators(setup_blockchain): assert isinstance( delegations, list )
validators = _test_staking_rpc(staking.get_validators, 2)
assert isinstance(validators, dict)
assert len(validators['validators']) > 0 def test_get_elected_validator_addresses( setup_blockchain ):
validator_addresses = _test_staking_rpc(
@pytest.mark.run(order=15) staking.get_elected_validator_addresses
def test_get_validator_keys(setup_blockchain): )
validators = _test_staking_rpc(staking.get_validator_keys, 2) assert isinstance( validator_addresses, list )
assert isinstance(validators, list) assert len( validator_addresses ) > 0
@pytest.mark.run(order=16)
def test_get_validator_self_delegation(setup_blockchain): def test_get_validators( setup_blockchain ):
self_delegation = _test_staking_rpc(staking.get_validator_self_delegation, test_validator_address) validators = _test_staking_rpc( staking.get_validators, 2 )
assert isinstance(self_delegation, int) assert isinstance( validators, dict )
assert len( validators[ "validators" ] ) > 0
def test_get_validator_keys( setup_blockchain ):
validators = _test_staking_rpc( staking.get_validator_keys, 2 )
assert isinstance( validators, list )
def test_get_validator_self_delegation( setup_blockchain ):
self_delegation = _test_staking_rpc(
staking.get_validator_self_delegation,
test_validator_address
)
assert isinstance( self_delegation, int )
assert self_delegation > 0 assert self_delegation > 0
@pytest.mark.run(order=17)
def test_get_validator_total_delegation(setup_blockchain): def test_get_validator_total_delegation( setup_blockchain ):
total_delegation = _test_staking_rpc(staking.get_validator_total_delegation, test_validator_address) total_delegation = _test_staking_rpc(
assert isinstance(total_delegation, int) staking.get_validator_total_delegation,
test_validator_address
)
assert isinstance( total_delegation, int )
assert total_delegation > 0 assert total_delegation > 0
@pytest.mark.run(order=18)
def test_get_all_delegation_information(setup_blockchain): def test_get_all_delegation_information( setup_blockchain ):
delegation_information = _test_staking_rpc(staking.get_all_delegation_information, 0) delegation_information = _test_staking_rpc(
assert isinstance(delegation_information, list) staking.get_all_delegation_information,
assert len(delegation_information) > 0 0
)
@pytest.mark.run(order=19) assert isinstance( delegation_information, list )
def test_get_delegation_by_delegator_and_validator(setup_blockchain): assert len( delegation_information ) > 0
delegation_information = _test_staking_rpc(staking.get_delegation_by_delegator_and_validator, test_validator_address, test_validator_address)
assert isinstance(delegation_information, dict)
def test_get_delegation_by_delegator_and_validator( setup_blockchain ):
@pytest.mark.run(order=20) delegation_information = _test_staking_rpc(
def test_get_available_redelegation_balance(setup_blockchain): staking.get_delegation_by_delegator_and_validator,
redelgation_balance = _test_staking_rpc(staking.get_available_redelegation_balance, test_validator_address) test_validator_address,
assert isinstance(redelgation_balance, int) test_validator_address,
)
assert isinstance( delegation_information, dict )
def test_get_available_redelegation_balance( setup_blockchain ):
redelgation_balance = _test_staking_rpc(
staking.get_available_redelegation_balance,
test_validator_address
)
assert isinstance( redelgation_balance, int )
assert redelgation_balance == 0 assert redelgation_balance == 0
@pytest.mark.run(order=21)
def test_get_total_staking(setup_blockchain): def test_get_total_staking( setup_blockchain ):
total_staking = _test_staking_rpc(staking.get_total_staking) total_staking = _test_staking_rpc( staking.get_total_staking )
assert isinstance(total_staking, int) assert isinstance( total_staking, int )
if (
staking.get_validator_information(
test_validator_address,
explorer_endpoint
)[ "active-status" ] == "active"
):
assert total_staking > 0 assert total_staking > 0
@pytest.mark.run(order=22)
def test_errors(): def test_errors():
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_all_validator_addresses(fake_shard) staking.get_all_validator_addresses( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_validator_information('', fake_shard) staking.get_validator_information( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_elected_validator_addresses(fake_shard) staking.get_elected_validator_addresses( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_validators(1, fake_shard) staking.get_validators( 1, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_validator_keys(1, fake_shard) staking.get_validator_keys( 1, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_validator_information_by_block_number('', 1, fake_shard) staking.get_validator_information_by_block_number( "", 1, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_all_validator_information(-1, fake_shard) staking.get_all_validator_information( 0, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_validator_self_delegation('', fake_shard) staking.get_validator_self_delegation( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_validator_total_delegation('', fake_shard) staking.get_validator_total_delegation( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_all_validator_information_by_block_number(1, 1, fake_shard) staking.get_all_validator_information_by_block_number(
with pytest.raises(exceptions.RPCError): 1,
staking.get_all_delegation_information(1, fake_shard) 1,
with pytest.raises(exceptions.RPCError): fake_shard
staking.get_delegations_by_delegator('', fake_shard) )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_delegations_by_delegator_by_block_number('', 1, fake_shard) staking.get_all_delegation_information( 1, fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_delegation_by_delegator_and_validator('', '', fake_shard) staking.get_delegations_by_delegator( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_available_redelegation_balance('', fake_shard) staking.get_delegations_by_delegator_by_block_number(
with pytest.raises(exceptions.RPCError): "",
staking.get_delegations_by_validator('', fake_shard) 1,
with pytest.raises(exceptions.RPCError): fake_shard
staking.get_current_utility_metrics(fake_shard) )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_staking_network_info(fake_shard) staking.get_delegation_by_delegator_and_validator( "", "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_super_committees(fake_shard) staking.get_available_redelegation_balance( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_total_staking(fake_shard) staking.get_delegations_by_validator( "", fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
staking.get_raw_median_stake_snapshot(fake_shard) staking.get_current_utility_metrics( fake_shard )
with pytest.raises( exceptions.RPCError ):
staking.get_staking_network_info( fake_shard )
with pytest.raises( exceptions.RPCError ):
staking.get_super_committees( fake_shard )
with pytest.raises( exceptions.RPCError ):
staking.get_total_staking( fake_shard )
with pytest.raises( exceptions.RPCError ):
staking.get_raw_median_stake_snapshot( fake_shard )

@ -1,15 +1,10 @@
from pyhmy import ( from pyhmy import staking_signing, staking_structures
staking_signing,
staking_structures
)
from pyhmy.numbers import ( from pyhmy.numbers import convert_one_to_atto
convert_one_to_atto
)
# other transactions (create/edit validator) are in test_validator.py # other transactions (create/edit validator) are in test_validator.py
# test_delegate is the same as test_undelegate (except the directive) so it has been omitted # test_delegate is the same as test_undelegate (except the directive) so it has been omitted
# staking transactions without a chain id have been omitted as well, since the node does not accept them anyway
""" """
let stakingTx let stakingTx
let stakeMsg3: CollectRewards = new CollectRewards( let stakeMsg3: CollectRewards = new CollectRewards(
@ -27,17 +22,16 @@ const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd
console.log( 'Signed transaction' ) console.log( 'Signed transaction' )
console.log(signed) console.log(signed)
""" """
def test_collect_rewards_no_chain_id(): # def test_collect_rewards_no_chain_id():
transaction_dict = { # transaction_dict = {
'directive': staking_structures.Directive.CollectRewards, # 'directive': staking_structures.Directive.CollectRewards,
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', # 'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9',
'nonce': 2, # 'nonce': 2,
'gasPrice': int(convert_one_to_atto(1)), # 'gasPrice': int(convert_one_to_atto(1)),
'gasLimit': 100, # 'gasLimit': 100,
} # }
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') # signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48')
assert signed_tx.rawTransaction.hex() == '0xf85a04d594ebcd16e8c1d8f493ba04e99a56474122d81a9c5823a0490e4ceb747563ba40da3e0db8a65133cf6f6ae4c48a24866cd6aa1f0d6c2414a06dbd51a67b35b5685e7b7420cba26e63b0e7d3c696fc6cb69d48e54fcad280e9' # assert signed_tx.rawTransaction.hex() == '0xf85a04d594ebcd16e8c1d8f493ba04e99a56474122d81a9c5823a0490e4ceb747563ba40da3e0db8a65133cf6f6ae4c48a24866cd6aa1f0d6c2414a06dbd51a67b35b5685e7b7420cba26e63b0e7d3c696fc6cb69d48e54fcad280e9'
""" """
let stakingTx let stakingTx
let stakeMsg3: CollectRewards = new CollectRewards( let stakeMsg3: CollectRewards = new CollectRewards(
@ -55,17 +49,26 @@ const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd
console.log( 'Signed transaction' ) console.log( 'Signed transaction' )
console.log(signed) console.log(signed)
""" """
def test_collect_rewards_chain_id(): def test_collect_rewards_chain_id():
transaction_dict = { transaction_dict = {
'directive': staking_structures.Directive.CollectRewards, "directive": staking_structures.Directive.CollectRewards,
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', "delegatorAddress": "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9",
'nonce': 2, "nonce": 2,
'gasPrice': int(convert_one_to_atto(1)), "gasPrice": int(convert_one_to_atto(1)),
'gasLimit': 100, "gasLimit": 100,
'chainId': 1, # with chainId for coverage "chainId": 1, # with chainId for coverage
} }
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') signed_tx = staking_signing.sign_staking_transaction(
assert signed_tx.rawTransaction.hex() == '0xf86504d594ebcd16e8c1d8f493ba04e99a56474122d81a9c5802880de0b6b3a76400006425a055d6c3c0d8e7a1e75152db361a2ed47f5ab54f6f19b0d8e549953dbdf13ba647a076e1367dfca38eae3bd0e8da296335acabbaeb87dc17e47ebe4942db29334099' transaction_dict,
"4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48",
)
assert (
signed_tx.rawTransaction.hex() ==
"0xf86504d594ebcd16e8c1d8f493ba04e99a56474122d81a9c5802880de0b6b3a76400006425a055d6c3c0d8e7a1e75152db361a2ed47f5ab54f6f19b0d8e549953dbdf13ba647a076e1367dfca38eae3bd0e8da296335acabbaeb87dc17e47ebe4942db29334099"
)
""" """
let stakingTx let stakingTx
@ -80,21 +83,30 @@ stakingTx = new StakingTransaction(
2, // nonce 2, // nonce
numberToHex(new Unit('1').asOne().toWei()), // gasPrice numberToHex(new Unit('1').asOne().toWei()), // gasPrice
100, // gasLimit 100, // gasLimit
null, // chainId 2, // chainId
); );
const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48')
console.log( 'Signed transaction' ) console.log( 'Signed transaction' )
console.log(signed) console.log(signed)
""" """
def test_delegate(): def test_delegate():
transaction_dict = { transaction_dict = {
'directive': staking_structures.Directive.Delegate, "directive": staking_structures.Directive.Delegate,
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', "delegatorAddress": "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9",
'validatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', "validatorAddress": "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9",
'amount': 5, "amount": 5,
'nonce': 2, "nonce": 2,
'gasPrice': int(convert_one_to_atto(1)), "gasPrice": int( convert_one_to_atto( 1 ) ),
'gasLimit': 100, "gasLimit": 100,
"chainId": 2,
} }
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') signed_tx = staking_signing.sign_staking_transaction(
assert signed_tx.rawTransaction.hex() == '0xf87002eb94ebcd16e8c1d8f493ba04e99a56474122d81a9c5894ebcd16e8c1d8f493ba04e99a56474122d81a9c580523a0aceff4166ec0ecd0cc664fed865270fe77b35e408138950f802129f1f3d06a74a06f9aca402fb6b4842bff8d65f430d82eefa95645e9046b102195d1044993f9fe' transaction_dict,
"4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48",
)
assert (
signed_tx.rawTransaction.hex() ==
"0xf87b02eb94ebcd16e8c1d8f493ba04e99a56474122d81a9c5894ebcd16e8c1d8f493ba04e99a56474122d81a9c580502880de0b6b3a76400006428a0c856fd483a989ca4db4b5257f6996729527828fb21ec13cc65f0bffe6c015ab1a05e9d3c92742e8cb7450bebdfb7ad277ccbfc9fa0719db0b12a715a0a173cadd6"
)

@ -1,218 +1,266 @@
import pytest import pytest
from pyhmy import ( from pyhmy import transaction
transaction
)
from pyhmy.rpc import ( from pyhmy.rpc import exceptions
exceptions
)
from pyhmy.exceptions import ( endpoint = "http://localhost:9500"
TxConfirmationTimedoutError endpoint_shard_one = "http://localhost:9502"
) fake_shard = "http://example.com"
# previously sent txs to get and check
localhost_shard_one = 'http://localhost:9501' tx_hash = "0xc26be5776aa57438bccf196671a2d34f3f22c9c983c0f844c62b2fb90403aa43"
tx_hash = '0x1fa20537ea97f162279743139197ecf0eac863278ac1c8ada9a6be5d1e31e633'
tx_block_num = None tx_block_num = None
tx_block_hash = None tx_block_hash = None
cx_hash = '0x1fa20537ea97f162279743139197ecf0eac863278ac1c8ada9a6be5d1e31e633' tx_index = None
stx_hash = '0x57ec011aabdeb078a4816502224022f291fa8b07c82bbae8476f514a1d71c730'
cx_hash = "0xf73ba634cb96fc0e3e2c9d3b4c91379e223741be4a5aa56e6d6caf49c1ae75cf"
stx_hash = "0xc8177ace2049d9f4eb4a45fd6bd6b16f693573d036322c36774cc00d05a3e24f"
stx_block_num = None stx_block_num = None
stx_block_hash = None stx_block_hash = None
test_index = 0 stx_index = None
fake_shard = 'http://example.com'
# new txs to send and check
raw_tx = "0xf86f0385174876e800825208808094c9c6d47ee5f2e3e08d7367ad1a1373ba9dd172418905b12aefafa80400008027a07a4952b90bf38723a9197179a8e6d2e9b3a86fd6da4e66a9cf09fdc59783f757a053910798b311245525bd77d6119332458c2855102e4fb9e564f6a3b710d18bb0"
raw_tx_hash = "0x7ccd80f8513f76ec58b357c7a82a12a95e025d88f1444e953f90e3d86e222571"
# raw_txt generated via: raw_stx = "0xf88302f494c9c6d47ee5f2e3e08d7367ad1a1373ba9dd1724194a5241513da9f4463f1d4874b548dfbac29d91f3489056bc75e2d631000008085174876e80082c35027a0808ea7d27adf3b1f561e8da4676814084bb75ac541b616bece87c6446e6cc54ea02f19f0b14240354bd42ad60b0c7189873c0be87044e13072b0981a837ca76f64"
# hmy transfer --from one12fuf7x9rgtdgqg7vgq0962c556m3p7afsxgvll --to one12fuf7x9rgtdgqg7vgq0962c556m3p7afsxgvll raw_stx_hash = "0xe7d07ef6d9fca595a14ceb0ca917bece7bedb15efe662300e9334a32ac1da629"
# --from-shard 0 --to-shard 1 --amount 0.1 --dry-run
raw_tx = '0xf86d01843b9aca0082520880019452789f18a342da8023cc401e5d2b14a6b710fba988016345785d8a00008028a01095f775386e0e3203446179a7a62e5ce1e765c200b5d885f6bb5b141155bd4da0651350a31e1797035cbf878e4c26069e9895845071d01308573532512cca5820'
raw_tx_hash = '0x86bce2e7765937b776bdcf927339c85421b95c70ddf06ba8e4cc0441142b0f53'
raw_stx = '0xf9015680f90105943ad89a684095a53edb47d7ddc5e034d813366731d984746573748474657374847465737484746573748474657374ddc988016345785d8a0000c9880c7d713b49da0000c887b1a2bc2ec500008a022385a827e8155000008b084595161401484a000000f1b0282554f2478661b4844a05a9deb1837aac83931029cb282872f0dcd7239297c499c02ea8da8746d2f08ca2b037e89891f862b86003557e18435c201ecc10b1664d1aea5b4ec59dbfe237233b953dbd9021b86bc9770e116ed3c413fe0334d89562568a10e133d828611f29fee8cdab9719919bbcc1f1bf812c73b9ccd0f89b4f0b9ca7e27e66d58bbb06fcf51c295b1d076cfc878a0228f16f86157860000080843b9aca008351220027a018385211a150ca032c3526cef0aba6a75f99a18cb73f547f67bab746be0c7a64a028be921002c6eb949b3932afd010dfe1de2459ec7fe84403b9d9d8892394a78c'
def _test_transaction_rpc(fn, *args, **kwargs): def _test_transaction_rpc( fn, *args, **kwargs ):
if not callable(fn): if not callable( fn ):
pytest.fail(f'Invalid function: {fn}') pytest.fail( f"Invalid function: {fn}" )
try: try:
response = fn(*args, **kwargs) response = fn( *args, **kwargs )
except Exception as e: except Exception as e:
if isinstance(e, exceptions.RPCError) and 'does not exist/is not available' in str(e): if isinstance( e,
pytest.skip(f'{str(e)}') exceptions.RPCError
if isinstance(e, TxConfirmationTimedoutError): ) and "does not exist/is not available" in str( e ):
pytest.skip(f'{str(e)}') pytest.skip( f"{str(e)}" )
pytest.fail(f'Unexpected error: {e.__class__} {e}') pytest.fail( f"Unexpected error: {e.__class__} {e}" )
return response return response
@pytest.mark.run(order=1)
def test_get_pending_transactions(setup_blockchain):
pool = _test_transaction_rpc(transaction.get_pending_transactions)
assert isinstance(pool, list)
@pytest.mark.run(order=2) def test_get_pending_transactions( setup_blockchain ):
def test_get_transaction_by_hash(setup_blockchain): pool = _test_transaction_rpc( transaction.get_pending_transactions )
tx = _test_transaction_rpc(transaction.get_transaction_by_hash, tx_hash, endpoint=localhost_shard_one) assert isinstance( pool, list )
def test_get_transaction_by_hash( setup_blockchain ):
tx = _test_transaction_rpc(
transaction.get_transaction_by_hash,
tx_hash,
endpoint = endpoint
)
assert tx assert tx
assert isinstance(tx, dict) assert isinstance( tx, dict )
assert 'blockNumber' in tx.keys() assert "blockNumber" in tx.keys()
assert 'blockHash' in tx.keys() assert "blockHash" in tx.keys()
global tx_block_num global tx_block_num
tx_block_num = int(tx['blockNumber']) tx_block_num = int( tx[ "blockNumber" ] )
global tx_block_hash global tx_block_hash
tx_block_hash = tx['blockHash'] tx_block_hash = tx[ "blockHash" ]
global tx_index
tx_index = int( tx[ "transactionIndex" ] )
@pytest.mark.run(order=3)
def test_get_transaction_by_block_hash_and_index(setup_blockchain): def test_get_transaction_by_block_hash_and_index( setup_blockchain ):
if not tx_block_hash: if not tx_block_hash:
pytest.skip('Failed to get reference block hash') pytest.skip( "Failed to get reference block hash" )
tx = _test_transaction_rpc(transaction.get_transaction_by_block_hash_and_index, tx = _test_transaction_rpc(
tx_block_hash, test_index, endpoint=localhost_shard_one) transaction.get_transaction_by_block_hash_and_index,
tx_block_hash,
tx_index,
endpoint = endpoint,
)
assert tx assert tx
assert isinstance(tx, dict) assert isinstance( tx, dict )
@pytest.mark.run(order=4) def test_get_transaction_by_block_number_and_index( setup_blockchain ):
def test_get_transaction_by_block_number_and_index(setup_blockchain):
if not tx_block_num: if not tx_block_num:
pytest.skip('Failed to get reference block num') pytest.skip( "Failed to get reference block num" )
tx = _test_transaction_rpc(transaction.get_transaction_by_block_number_and_index, tx_block_num, test_index, tx = _test_transaction_rpc(
endpoint=localhost_shard_one) transaction.get_transaction_by_block_number_and_index,
tx_block_num,
tx_index,
endpoint = endpoint,
)
assert tx assert tx
assert isinstance(tx, dict) assert isinstance( tx, dict )
@pytest.mark.run(order=5)
def test_get_transaction_receipt(setup_blockchain): def test_get_transaction_receipt( setup_blockchain ):
tx_receipt = _test_transaction_rpc(transaction.get_transaction_receipt, tx_hash, endpoint=localhost_shard_one) tx_receipt = _test_transaction_rpc(
transaction.get_transaction_receipt,
tx_hash,
endpoint = endpoint
)
assert tx_receipt assert tx_receipt
assert isinstance(tx_receipt, dict) assert isinstance( tx_receipt, dict )
@pytest.mark.run(order=6) def test_get_transaction_error_sink( setup_blockchain ):
def test_get_transaction_error_sink(setup_blockchain): errors = _test_transaction_rpc( transaction.get_transaction_error_sink )
errors = _test_transaction_rpc(transaction.get_transaction_error_sink) assert isinstance( errors, list )
assert isinstance(errors, list)
@pytest.mark.run(order=7)
def test_send_and_confirm_raw_transaction(setup_blockchain): def test_send_and_confirm_raw_transaction( setup_blockchain ):
# Note: this test is not yet idempotent since the localnet will reject transactions which were previously finalized. # Note: this test is not yet idempotent since the localnet will reject transactions which were previously finalized.
# Secondly, this is a test that seems to return None values - for example the below curl call has the same null value test_tx = _test_transaction_rpc(
# curl --location --request POST 'http://localhost:9501' \ transaction.send_and_confirm_raw_transaction,
# --header 'Content-Type: application/json' \ raw_tx
# --data-raw '{ )
# "jsonrpc": "2.0", assert isinstance( test_tx, dict )
# "id": 1, assert test_tx[ "hash" ] == raw_tx_hash
# "method": "hmyv2_getTransactionByHash",
# "params": [
# "0x86bce2e7765937b776bdcf927339c85421b95c70ddf06ba8e4cc0441142b0f53" def test_get_pending_cx_receipts( setup_blockchain ):
# ] pending = _test_transaction_rpc( transaction.get_pending_cx_receipts )
# }' assert isinstance( pending, list )
# {"jsonrpc":"2.0","id":1,"result":null}
test_tx = _test_transaction_rpc(transaction.send_and_confirm_raw_transaction,
raw_tx) # mining stops by the time this transaction is submitted def test_get_cx_receipt_by_hash( setup_blockchain ):
# so it never confirms, which is why TxConfirmationTimedoutError cx = _test_transaction_rpc(
# is in the set up call transaction.get_cx_receipt_by_hash,
assert isinstance(test_tx, dict) cx_hash,
assert test_tx[ 'hash' ] == raw_tx_hash endpoint_shard_one
)
@pytest.mark.run(order=8)
def test_get_pending_cx_receipts(setup_blockchain):
pending = _test_transaction_rpc(transaction.get_pending_cx_receipts)
assert isinstance(pending, list)
@pytest.mark.run(order=9)
def test_get_cx_receipt_by_hash(setup_blockchain):
cx = _test_transaction_rpc(transaction.get_cx_receipt_by_hash, cx_hash)
assert cx assert cx
assert isinstance(cx, dict) assert isinstance( cx, dict )
@pytest.mark.run(order=10)
def test_resend_cx_receipt(setup_blockchain):
sent = _test_transaction_rpc(transaction.resend_cx_receipt, cx_hash)
assert isinstance(sent, bool)
assert not sent
@pytest.mark.run(order=11) def test_resend_cx_receipt( setup_blockchain ):
def test_get_staking_transaction_by_hash(setup_blockchain): sent = _test_transaction_rpc( transaction.resend_cx_receipt, cx_hash )
staking_tx = _test_transaction_rpc(transaction.get_staking_transaction_by_hash, stx_hash) assert isinstance( sent, bool )
assert sent
def test_get_staking_transaction_by_hash( setup_blockchain ):
staking_tx = _test_transaction_rpc(
transaction.get_staking_transaction_by_hash,
stx_hash
)
assert staking_tx assert staking_tx
assert isinstance(staking_tx, dict) assert isinstance( staking_tx, dict )
assert 'blockNumber' in staking_tx.keys() assert "blockNumber" in staking_tx.keys()
assert 'blockHash' in staking_tx.keys() assert "blockHash" in staking_tx.keys()
global stx_block_num global stx_block_num
stx_block_num = int(staking_tx['blockNumber']) stx_block_num = int( staking_tx[ "blockNumber" ] )
global stx_block_hash global stx_block_hash
stx_block_hash = staking_tx['blockHash'] stx_block_hash = staking_tx[ "blockHash" ]
global stx_index
stx_index = int( staking_tx[ "transactionIndex" ] )
@pytest.mark.run(order=12)
def test_get_transaction_by_block_hash_and_index(setup_blockchain): def test_get_transaction_by_block_hash_and_index( setup_blockchain ):
if not stx_block_hash: if not stx_block_hash:
pytest.skip('Failed to get reference block hash') pytest.skip( "Failed to get reference block hash" )
stx = _test_transaction_rpc(transaction.get_staking_transaction_by_block_hash_and_index, stx_block_hash, test_index) stx = _test_transaction_rpc(
transaction.get_staking_transaction_by_block_hash_and_index,
stx_block_hash,
stx_index,
)
assert stx assert stx
assert isinstance(stx, dict) assert isinstance( stx, dict )
@pytest.mark.run(order=13) def test_get_transaction_by_block_number_and_index( setup_blockchain ):
def test_get_transaction_by_block_number_and_index(setup_blockchain):
if not stx_block_num: if not stx_block_num:
pytest.skip('Failed to get reference block num') pytest.skip( "Failed to get reference block num" )
stx = _test_transaction_rpc(transaction.get_staking_transaction_by_block_number_and_index, stx_block_num, test_index) stx = _test_transaction_rpc(
transaction.get_staking_transaction_by_block_number_and_index,
stx_block_num,
stx_index,
)
assert stx assert stx
assert isinstance(stx, dict) assert isinstance( stx, dict )
@pytest.mark.run(order=14)
def test_get_staking_transaction_error_sink(setup_blockchain): def test_get_staking_transaction_error_sink( setup_blockchain ):
errors = _test_transaction_rpc(transaction.get_staking_transaction_error_sink) errors = _test_transaction_rpc(
assert isinstance(errors, list) transaction.get_staking_transaction_error_sink
)
@pytest.mark.run(order=15) assert isinstance( errors, list )
def test_send_raw_staking_transaction(setup_blockchain):
test_stx_hash = _test_transaction_rpc(transaction.send_raw_staking_transaction, raw_stx, endpoint=localhost_shard_one)
assert isinstance(test_stx_hash, str) def test_send_raw_staking_transaction( setup_blockchain ):
assert test_stx_hash == stx_hash test_stx = _test_transaction_rpc(
transaction.send_and_confirm_raw_staking_transaction,
@pytest.mark.run(order=16) raw_stx,
def test_get_pool_stats(setup_blockchain): endpoint = endpoint
test_pool_stats = _test_transaction_rpc(transaction.get_pool_stats, endpoint=localhost_shard_one) )
assert isinstance(test_pool_stats, dict) assert isinstance( test_stx, dict )
assert test_stx[ "hash" ] == raw_stx_hash
@pytest.mark.run(order=17)
def test_get_pending_staking_transactions(setup_blockchain):
pending_staking_transactions = _test_transaction_rpc(transaction.get_pending_staking_transactions, endpoint=localhost_shard_one) def test_get_pool_stats( setup_blockchain ):
assert isinstance(pending_staking_transactions, list) test_pool_stats = _test_transaction_rpc(
transaction.get_pool_stats,
@pytest.mark.run(order=18) endpoint = endpoint
)
assert isinstance( test_pool_stats, dict )
def test_get_pending_staking_transactions( setup_blockchain ):
pending_staking_transactions = _test_transaction_rpc(
transaction.get_pending_staking_transactions,
endpoint = endpoint
)
assert isinstance( pending_staking_transactions, list )
def test_errors(): def test_errors():
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_pending_transactions(fake_shard) transaction.get_pending_transactions( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_transaction_error_sink(fake_shard) transaction.get_transaction_error_sink( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_pool_stats(fake_shard) transaction.get_pool_stats( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_transaction_by_hash('', endpoint=fake_shard) transaction.get_transaction_by_hash( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_transaction_by_block_hash_and_index('', 1, endpoint=fake_shard) transaction.get_transaction_by_block_hash_and_index(
with pytest.raises(exceptions.RPCError): "",
transaction.get_transaction_by_block_number_and_index(1, 1, endpoint=fake_shard) 1,
with pytest.raises(exceptions.RPCError): endpoint = fake_shard
transaction.get_transaction_receipt('', endpoint=fake_shard) )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.send_raw_transaction('', endpoint=fake_shard) transaction.get_transaction_by_block_number_and_index(
with pytest.raises(exceptions.RPCError): 1,
transaction.get_pending_cx_receipts(fake_shard) 1,
with pytest.raises(exceptions.RPCError): endpoint = fake_shard
transaction.get_cx_receipt_by_hash('', endpoint=fake_shard) )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.resend_cx_receipt('', endpoint=fake_shard) transaction.get_transaction_receipt( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_staking_transaction_by_hash('', endpoint=fake_shard) transaction.send_raw_transaction( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_staking_transaction_by_block_hash_and_index('', 1, endpoint=fake_shard) transaction.get_pending_cx_receipts( fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_staking_transaction_by_block_number_and_index(1, 1, endpoint=fake_shard) transaction.get_cx_receipt_by_hash( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_staking_transaction_error_sink(endpoint=fake_shard) transaction.resend_cx_receipt( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.send_raw_staking_transaction('', endpoint=fake_shard) transaction.get_staking_transaction_by_hash( "", endpoint = fake_shard )
with pytest.raises(exceptions.RPCError): with pytest.raises( exceptions.RPCError ):
transaction.get_pending_staking_transactions(endpoint=fake_shard) transaction.get_staking_transaction_by_block_hash_and_index(
"",
1,
endpoint = fake_shard
)
with pytest.raises( exceptions.RPCError ):
transaction.get_staking_transaction_by_block_number_and_index(
1,
1,
endpoint = fake_shard
)
with pytest.raises( exceptions.RPCError ):
transaction.get_staking_transaction_error_sink( endpoint = fake_shard )
with pytest.raises( exceptions.RPCError ):
transaction.send_raw_staking_transaction( "", endpoint = fake_shard )
with pytest.raises( exceptions.RPCError ):
transaction.get_pending_staking_transactions( endpoint = fake_shard )

@ -1,26 +1,11 @@
import pytest import pytest
import requests from decimal import Decimal
from decimal import (
Decimal
)
from pyhmy import (
validator
)
from pyhmy.rpc import ( from pyhmy import validator
exceptions
)
from pyhmy.numbers import ( from pyhmy.numbers import convert_one_to_atto
convert_one_to_atto
)
from pyhmy.exceptions import ( from pyhmy.exceptions import InvalidValidatorError
InvalidValidatorError
)
import sys
test_epoch_number = 0 test_epoch_number = 0
genesis_block_number = 0 genesis_block_number = 0
@ -28,36 +13,46 @@ test_block_number = 1
test_validator_object = None test_validator_object = None
test_validator_loaded = False test_validator_loaded = False
@pytest.mark.run(order=0)
def test_instantiate_validator(setup_blockchain): def test_instantiate_validator( setup_blockchain ):
global test_validator_object global test_validator_object
test_validator_object = validator.Validator('one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9') test_validator_object = validator.Validator(
assert isinstance(test_validator_object, validator.Validator) "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9"
)
assert isinstance( test_validator_object, validator.Validator )
@pytest.mark.run(order=1) def test_load_validator( setup_blockchain ):
def test_load_validator(setup_blockchain):
if not test_validator_object: if not test_validator_object:
pytest.skip('Validator not instantiated yet') pytest.skip( "Validator not instantiated yet" )
info = { info = {
'name': 'Alice', "name": "Alice",
'identity': 'alice', "identity": "alice",
'website': 'alice.harmony.one', "website": "alice.harmony.one",
'details': "Don't mess with me!!!", "details": "Don't mess with me!!!",
'security-contact': 'Bob', "security-contact": "Bob",
'min-self-delegation': convert_one_to_atto(10000), "min-self-delegation": convert_one_to_atto( 10000 ),
'amount': convert_one_to_atto(10001), "amount": convert_one_to_atto( 10001 ),
'max-rate': '0.9', "max-rate": "0.9",
'max-change-rate': '0.05', "max-change-rate": "0.05",
'rate': '0.01', "rate": "0.01",
'bls-public-keys': ['0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611'], "bls-public-keys": [
'max-total-delegation': convert_one_to_atto(40000) "0x30b2c38b1316da91e068ac3bd8751c0901ef6c02a1d58bc712104918302c6ed03d5894671d0c816dad2b4d303320f202"
],
"bls-key-sigs": [
"0x68f800b6adf657b674903e04708060912b893b7c7b500788808247550ab3e186e56a44ebf3ca488f8ed1a42f6cef3a04bd5d2b2b7eb5a767848d3135b362e668ce6bba42c7b9d5666d8e3a83be707b5708e722c58939fe9b07c170f3b7062414"
],
"max-total-delegation": convert_one_to_atto( 40000 ),
} }
test_validator_object.load(info) test_validator_object.load( info )
global test_validator_loaded global test_validator_loaded
test_validator_loaded = True test_validator_loaded = True
""" """
TypeScript signature source TypeScript signature source (is outdated because the JS SDK has not been updated for SlotKeySigs)
For now I have checked that the below transaction to localnet works
---
const description: Description = new Description('Alice', 'alice', 'alice.harmony.one', 'Bob', "Don't mess with me!!!") const description: Description = new Description('Alice', 'alice', 'alice.harmony.one', 'Bob', "Don't mess with me!!!")
const commissionRates: CommissionRate = new CommissionRate(new Decimal('0.01'), new Decimal('0.9'), new Decimal('0.05')) const commissionRates: CommissionRate = new CommissionRate(new Decimal('0.01'), new Decimal('0.9'), new Decimal('0.05'))
const stakeMsg: CreateValidator = new CreateValidator( const stakeMsg: CreateValidator = new CreateValidator(
@ -81,20 +76,28 @@ const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd
console.log( 'Signed transaction' ) console.log( 'Signed transaction' )
console.log(signed) console.log(signed)
""" """
@pytest.mark.run(order=2)
def test_create_validator_sign(setup_blockchain):
if not (test_validator_object or test_validator_loaded): def test_create_validator_sign( setup_blockchain ):
pytest.skip('Validator not ready yet') if not ( test_validator_object or test_validator_loaded ):
pytest.skip( "Validator not instantiated yet" )
signed_hash = test_validator_object.sign_create_validator_transaction( signed_hash = test_validator_object.sign_create_validator_transaction(
2, 2,
int(convert_one_to_atto(1)), int( convert_one_to_atto( 1 ) ),
100, 100,
'4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48', "4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48",
None).rawTransaction.hex() 2,
assert signed_hash == '0xf9010580f8bf94ebcd16e8c1d8f493ba04e99a56474122d81a9c58f83885416c69636585616c69636591616c6963652e6861726d6f6e792e6f6e6583426f6295446f6e2774206d6573732077697468206d65212121dcc8872386f26fc10000c9880c7d713b49da0000c887b1a2bc2ec500008a021e19e0c9bab24000008a0878678326eac9000000f1b0b9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b622476086118a021e27c1806e59a4000024a047c6d444971d4d3c48e8b255aa0e543ebb47b60f761582694e5af5330445aba5a04db1ffea9cca9f9e56e8f782c689db680992903acfd9c06f4593f7fd9a781bd7' ).rawTransaction.hex()
assert (
signed_hash ==
"0xf9017580f9012394ebcd16e8c1d8f493ba04e99a56474122d81a9c58f83885416c69636585616c69636591616c6963652e6861726d6f6e792e6f6e6583426f6295446f6e2774206d6573732077697468206d65212121dcc8872386f26fc10000c9880c7d713b49da0000c887b1a2bc2ec500008a021e19e0c9bab24000008a0878678326eac9000000f1b030b2c38b1316da91e068ac3bd8751c0901ef6c02a1d58bc712104918302c6ed03d5894671d0c816dad2b4d303320f202f862b86068f800b6adf657b674903e04708060912b893b7c7b500788808247550ab3e186e56a44ebf3ca488f8ed1a42f6cef3a04bd5d2b2b7eb5a767848d3135b362e668ce6bba42c7b9d5666d8e3a83be707b5708e722c58939fe9b07c170f3b70624148a021e27c1806e59a4000002880de0b6b3a76400006428a0c6c7e62f02331df0afd4699ec514a2fc4548c920d77ad74d98caeec8c924c09aa02b27b999a724b1d341d6bbb0e877611d0047542cb7e380f9a6a272d204b450cd"
)
""" """
Signature matched from TypeScript Signature matched from TypeScript (is outdated because the JS SDK has not been updated for SlotKeyToAddSig)
For now I have checked that the below transaction to localnet works
---
import { import {
CreateValidator, CreateValidator,
EditValidator, EditValidator,
@ -132,74 +135,91 @@ const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd
console.log( 'Signed transaction' ) console.log( 'Signed transaction' )
console.log(signed) console.log(signed)
""" """
@pytest.mark.run(order=3)
def test_edit_validator_sign(setup_blockchain):
if not (test_validator_object or test_validator_loaded): def test_edit_validator_sign( setup_blockchain ):
pytest.skip('Validator not ready yet') if not ( test_validator_object or test_validator_loaded ):
pytest.skip( "Validator not instantiated yet" )
signed_hash = test_validator_object.sign_edit_validator_transaction( signed_hash = test_validator_object.sign_edit_validator_transaction(
2, 2,
int(convert_one_to_atto(1)), int(convert_one_to_atto(1)),
100, 100,
'0.06', "0.06",
'0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608612', # add key "0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608612", # remove key
"0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611", # remove key "0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611", # add key
'4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48', "0x68f800b6adf657b674903e04708060912b893b7c7b500788808247550ab3e186e56a44ebf3ca488f8ed1a42f6cef3a04bd5d2b2b7eb5a767848d3135b362e668ce6bba42c7b9d5666d8e3a83be707b5708e722c58939fe9b07c170f3b7062414", # add key sig
2).rawTransaction.hex() "4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48",
assert signed_hash == '0xf9012101f8d094ebcd16e8c1d8f493ba04e99a56474122d81a9c58f83885416c69636585616c69636591616c6963652e6861726d6f6e792e6f6e6583426f6295446f6e2774206d6573732077697468206d65212121c887d529ae9e8600008a021e19e0c9bab24000008a0878678326eac9000000b0b9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611b0b9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b6224760861202880de0b6b3a76400006428a0656d6741687ec1e42d1699274584a1777964e939b0ef11f3ff0e161859da21a2a03fc51e067f9fb6c96bee5ceccad4104f5b4b334a86a36a2f53d10b9a8e4a268a' 2,
).rawTransaction.hex()
@pytest.mark.run(order=4) assert (
def test_invalid_validator(setup_blockchain): signed_hash ==
if not (test_validator_object or test_validator_loaded): "0xf9018401f9013294ebcd16e8c1d8f493ba04e99a56474122d81a9c58f83885416c69636585616c69636591616c6963652e6861726d6f6e792e6f6e6583426f6295446f6e2774206d6573732077697468206d65212121c887d529ae9e8600008a021e19e0c9bab24000008a0878678326eac9000000b0b9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608612b0b9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611b86068f800b6adf657b674903e04708060912b893b7c7b500788808247550ab3e186e56a44ebf3ca488f8ed1a42f6cef3a04bd5d2b2b7eb5a767848d3135b362e668ce6bba42c7b9d5666d8e3a83be707b5708e722c58939fe9b07c170f3b706241402880de0b6b3a76400006427a0ecdae4a29d051f4f83dd54004858fbf0f7820e169b8e1846245835ceb686ee12a04b2336eb5830e30720137b2de539518fd5655467fef140ab31fde881a19f256a"
pytest.skip('Validator not ready yet') )
with pytest.raises(InvalidValidatorError):
def test_invalid_validator( setup_blockchain ):
if not ( test_validator_object or test_validator_loaded ):
pytest.skip( "Validator not instantiated yet" )
with pytest.raises( InvalidValidatorError ):
info = { info = {
'name': 'Alice', "name": "Alice",
} }
test_validator_object.load(info) test_validator_object.load( info )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_name('a'*141) test_validator_object.set_name( "a" * 141 )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_identity('a'*141) test_validator_object.set_identity( "a" * 141 )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_website('a'*141) test_validator_object.set_website( "a" * 141 )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_security_contact('a'*141) test_validator_object.set_security_contact( "a" * 141 )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_details('a'*281) test_validator_object.set_details( "a" * 281 )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_min_self_delegation(1) test_validator_object.set_min_self_delegation( 1 )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_max_total_delegation(1) test_validator_object.set_max_total_delegation( 1 )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_amount(1) test_validator_object.set_amount( 1 )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_max_rate('2.0') test_validator_object.set_max_rate( "2.0" )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_max_change_rate('-2.0') test_validator_object.set_max_change_rate( "-2.0" )
with pytest.raises(InvalidValidatorError): with pytest.raises( InvalidValidatorError ):
test_validator_object.set_rate('-2.0') test_validator_object.set_rate( "-2.0" )
@pytest.mark.run(order=5)
def test_validator_getters(setup_blockchain): def test_validator_getters( setup_blockchain ):
if not (test_validator_object or test_validator_loaded): if not ( test_validator_object or test_validator_loaded ):
pytest.skip('Validator not ready yet') pytest.skip( "Validator not instantiated yet" )
assert test_validator_object.get_address() == 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9' assert (
assert test_validator_object.add_bls_key('5') test_validator_object.get_address() ==
assert test_validator_object.remove_bls_key('5') "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9"
assert test_validator_object.get_name() == 'Alice' )
assert test_validator_object.get_identity() == 'alice' assert test_validator_object.add_bls_key( "5" )
assert test_validator_object.get_website() == 'alice.harmony.one' assert test_validator_object.remove_bls_key( "5" )
assert test_validator_object.get_security_contact() == 'Bob' assert test_validator_object.get_name() == "Alice"
assert test_validator_object.get_identity() == "alice"
assert test_validator_object.get_website() == "alice.harmony.one"
assert test_validator_object.get_security_contact() == "Bob"
assert test_validator_object.get_details() == "Don't mess with me!!!" assert test_validator_object.get_details() == "Don't mess with me!!!"
assert isinstance(test_validator_object.get_min_self_delegation(), Decimal) assert isinstance(
assert isinstance(test_validator_object.get_max_total_delegation(), Decimal) test_validator_object.get_min_self_delegation(),
assert isinstance(test_validator_object.get_amount(), Decimal) Decimal
assert isinstance(test_validator_object.get_max_rate(), Decimal) )
assert isinstance(test_validator_object.get_max_change_rate(), Decimal) assert isinstance(
assert isinstance(test_validator_object.get_rate(), Decimal) test_validator_object.get_max_total_delegation(),
assert len(test_validator_object.get_bls_keys()) > 0 Decimal
)
@pytest.mark.run(order=6) assert isinstance( test_validator_object.get_amount(), Decimal )
def test_validator_load_from_blockchain(setup_blockchain): assert isinstance( test_validator_object.get_max_rate(), Decimal )
test_validator_object2 = validator.Validator('one109r0tns7av5sjew7a7fkekg4fs3pw0h76pp45e') assert isinstance( test_validator_object.get_max_change_rate(), Decimal )
assert isinstance( test_validator_object.get_rate(), Decimal )
assert len( test_validator_object.get_bls_keys() ) > 0
def test_validator_load_from_blockchain( setup_blockchain ):
test_validator_object2 = validator.Validator(
"one155jp2y76nazx8uw5sa94fr0m4s5aj8e5xm6fu3"
)
test_validator_object2.load_from_blockchain() test_validator_object2.load_from_blockchain()

@ -12,43 +12,64 @@ from pyhmy import util
TEMP_DIR = "/tmp/pyhmy-testing/test-util" TEMP_DIR = "/tmp/pyhmy-testing/test-util"
@pytest.fixture(scope="session", autouse=True) @pytest.fixture( scope = "session", autouse = True )
def setup(): def setup():
shutil.rmtree(TEMP_DIR, ignore_errors=True) shutil.rmtree( TEMP_DIR, ignore_errors = True )
os.makedirs(TEMP_DIR, exist_ok=True) os.makedirs( TEMP_DIR, exist_ok = True )
def test_json_load(): def test_json_load():
dec = util.json_load('1.1', parse_float=decimal.Decimal) dec = util.json_load( "1.1", parse_float = decimal.Decimal )
assert isinstance(dec, decimal.Decimal) assert isinstance( dec, decimal.Decimal )
assert float(dec) == 1.1 assert float( dec ) == 1.1
ref_dict = { ref_dict = {
'test': 'val', "test": "val",
'arr': [ "arr": [ 1,
1,
2, 2,
3 3 ]
]
} }
loaded_dict = util.json_load(json.dumps(ref_dict)) loaded_dict = util.json_load( json.dumps( ref_dict ) )
assert str(ref_dict) == str(loaded_dict) assert str( ref_dict ) == str( loaded_dict )
def test_chain_id_to_int(): def test_chain_id_to_int():
assert util.chain_id_to_int(2) == 2 assert util.chain_id_to_int( 2 ) == 2
assert util.chain_id_to_int('HmyMainnet') == 1 assert util.chain_id_to_int( "HmyMainnet" ) == 1
def test_get_gopath(): def test_get_gopath():
assert isinstance(util.get_gopath(), str) assert isinstance( util.get_gopath(), str )
def test_get_goversion(): def test_get_goversion():
assert isinstance(util.get_goversion(), str) assert isinstance( util.get_goversion(), str )
def test_convert_one_to_hex(): def test_convert_one_to_hex():
assert util.convert_one_to_hex('0xebcd16e8c1d8f493ba04e99a56474122d81a9c58') == '0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58' assert (
assert util.convert_one_to_hex('one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9') == '0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58' util.convert_one_to_hex( "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" )
== "0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58"
)
assert (
util.convert_one_to_hex( "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9" )
== "0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58"
)
def test_convert_hex_to_one():
assert (
util.convert_hex_to_one( "0xebcd16e8c1d8f493ba04e99a56474122d81a9c58" )
== "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9"
)
assert (
util.convert_hex_to_one( "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9" )
== "one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9"
)
def test_get_bls_build_variables(): def test_get_bls_build_variables():
assert isinstance(util.get_bls_build_variables(), dict) assert isinstance( util.get_bls_build_variables(), dict )
def test_is_active_shard(): def test_is_active_shard():
assert isinstance(util.is_active_shard(''), bool) assert isinstance( util.is_active_shard( "" ), bool )

Loading…
Cancel
Save