Merge pull request #20 from MaxMustermann2/master
Resolve Bounty 7 (improve Harmony Python SDK)pull/22/head
commit
099274bec1
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,240 @@ |
|||||||
|
from .rpc.request import ( |
||||||
|
rpc_request |
||||||
|
) |
||||||
|
|
||||||
|
from .transaction import ( |
||||||
|
get_transaction_receipt |
||||||
|
) |
||||||
|
|
||||||
|
|
||||||
|
_default_endpoint = 'http://localhost:9500' |
||||||
|
_default_timeout = 30 |
||||||
|
|
||||||
|
|
||||||
|
######################### |
||||||
|
# Smart contract RPCs |
||||||
|
######################### |
||||||
|
def call(to, block_num, 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 |
||||||
|
---------- |
||||||
|
to: :obj:`str` |
||||||
|
Address of the smart contract |
||||||
|
block_num: :obj:`int` |
||||||
|
Block number to execute the contract for |
||||||
|
from_address: :obj:`str`, optional |
||||||
|
Wallet address |
||||||
|
gas: :obj:`str`, optional |
||||||
|
Gas to execute the smart contract (in hex) |
||||||
|
gas_price: :obj:`str`, optional |
||||||
|
Gas price to execute smart contract call (in hex) |
||||||
|
value: :obj:`str`, optional |
||||||
|
Value sent with the smart contract call (in hex) |
||||||
|
data: :obj:`str`, optional |
||||||
|
Hash of smart contract method and parameters |
||||||
|
endpoint: :obj:`str`, optional |
||||||
|
Endpoint to send request to |
||||||
|
timeout: :obj:`int`, optional |
||||||
|
Timeout in seconds |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
str |
||||||
|
Return value of the executed smart contract |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
InvalidRPCReplyError |
||||||
|
If received unknown result from endpoint, or |
||||||
|
|
||||||
|
API Reference |
||||||
|
------------- |
||||||
|
https://api.hmny.io/?version=latest#d34b1f82-9b29-4b68-bac7-52fa0a8884b1 |
||||||
|
""" |
||||||
|
params = [ |
||||||
|
{ |
||||||
|
'to': to, |
||||||
|
'from': from_address, |
||||||
|
'gas': gas, |
||||||
|
'gasPrice': gas_price, |
||||||
|
'value': value, |
||||||
|
'data': data |
||||||
|
}, |
||||||
|
block_num |
||||||
|
] |
||||||
|
method = 'hmyv2_call' |
||||||
|
try: |
||||||
|
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] |
||||||
|
except KeyError as e: |
||||||
|
raise InvalidRPCReplyError(method, endpoint) from e |
||||||
|
|
||||||
|
def estimate_gas(to, 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 |
||||||
|
---------- |
||||||
|
to: :obj:`str` |
||||||
|
Address of the smart contract |
||||||
|
from_address: :obj:`str`, optional |
||||||
|
Wallet address |
||||||
|
gas: :obj:`str`, optional |
||||||
|
Gas to execute the smart contract (in hex) |
||||||
|
gas_price: :obj:`str`, optional |
||||||
|
Gas price to execute smart contract call (in hex) |
||||||
|
value: :obj:`str`, optional |
||||||
|
Value sent with the smart contract call (in hex) |
||||||
|
data: :obj:`str`, optional |
||||||
|
Hash of smart contract method and parameters |
||||||
|
endpoint: :obj:`str`, optional |
||||||
|
Endpoint to send request to |
||||||
|
timeout: :obj:`int`, optional |
||||||
|
Timeout in seconds |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
int |
||||||
|
Estimated gas price of smart contract call |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
InvalidRPCReplyError |
||||||
|
If received unknown result from endpoint, or |
||||||
|
|
||||||
|
API Reference |
||||||
|
------------- |
||||||
|
https://api.hmny.io/?version=latest#b9bbfe71-8127-4dda-b26c-ff95c4c22abd |
||||||
|
""" |
||||||
|
params = [ { |
||||||
|
'to': to, |
||||||
|
'from': from_address, |
||||||
|
'gas': gas, |
||||||
|
'gasPrice': gas_price, |
||||||
|
'value': value, |
||||||
|
'data': data |
||||||
|
} ] |
||||||
|
method = 'hmyv2_estimateGas' |
||||||
|
try: |
||||||
|
return int(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'], 16) |
||||||
|
except KeyError as e: |
||||||
|
raise InvalidRPCReplyError(method, endpoint) from e |
||||||
|
|
||||||
|
def get_code(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 |
||||||
|
---------- |
||||||
|
address: :obj:`str` |
||||||
|
Address of the smart contract |
||||||
|
block_num: :obj:`int` |
||||||
|
Block number to get the code for |
||||||
|
endpoint: :obj:`str`, optional |
||||||
|
Endpoint to send request to |
||||||
|
timeout: :obj:`int`, optional |
||||||
|
Timeout in seconds |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
str |
||||||
|
Byte code at the smart contract address for the given block |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
InvalidRPCReplyError |
||||||
|
If received unknown result from endpoint, or |
||||||
|
|
||||||
|
API Reference |
||||||
|
------------- |
||||||
|
https://api.hmny.io/?version=latest#e13e9d78-9322-4dc8-8917-f2e721a8e556 |
||||||
|
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/contract.go#L59 |
||||||
|
""" |
||||||
|
params = [ |
||||||
|
address, |
||||||
|
block_num |
||||||
|
] |
||||||
|
method = 'hmyv2_getCode' |
||||||
|
try: |
||||||
|
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] |
||||||
|
except KeyError as e: |
||||||
|
raise InvalidRPCReplyError(method, endpoint) from e |
||||||
|
|
||||||
|
def get_storage_at(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 |
||||||
|
---------- |
||||||
|
address: :obj:`str` |
||||||
|
Address of the smart contract |
||||||
|
key: :obj:`str` |
||||||
|
Hex representation of the storage location |
||||||
|
block_num: :obj:`int` |
||||||
|
Block number to get the code for |
||||||
|
endpoint: :obj:`str`, optional |
||||||
|
Endpoint to send request to |
||||||
|
timeout: :obj:`int`, optional |
||||||
|
Timeout in seconds |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
str |
||||||
|
Data stored at the smart contract location |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
InvalidRPCReplyError |
||||||
|
If received unknown result from endpoint, or |
||||||
|
|
||||||
|
API Reference |
||||||
|
------------- |
||||||
|
https://api.hmny.io/?version=latest#fa8ac8bd-952d-4149-968c-857ca76da43f |
||||||
|
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/contract.go#L84 |
||||||
|
""" |
||||||
|
params = [ |
||||||
|
address, |
||||||
|
key, |
||||||
|
block_num |
||||||
|
] |
||||||
|
method = 'hmyv2_getStorageAt' |
||||||
|
try: |
||||||
|
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] |
||||||
|
except KeyError as e: |
||||||
|
raise InvalidRPCReplyError(method, endpoint) from e |
||||||
|
|
||||||
|
def get_contract_address_from_hash(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 |
||||||
|
---------- |
||||||
|
tx_hash: :obj:`str` |
||||||
|
Hash of the deployment transaction |
||||||
|
endpoint: :obj:`str`, optional |
||||||
|
Endpoint to send request to |
||||||
|
timeout: :obj:`int`, optional |
||||||
|
Timeout in seconds |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
str |
||||||
|
Address of the smart contract |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
InvalidRPCReplyError |
||||||
|
If received unknown result from endpoint, or |
||||||
|
|
||||||
|
API Reference |
||||||
|
------------- |
||||||
|
https://github.com/harmony-one/harmony-test/blob/master/localnet/rpc_tests/test_contract.py#L36 |
||||||
|
""" |
||||||
|
try: |
||||||
|
return get_transaction_receipt(tx_hash, endpoint, timeout)["contractAddress"] |
||||||
|
except KeyError as e: |
||||||
|
raise InvalidRPCReplyError(method, endpoint) from e |
@ -0,0 +1,200 @@ |
|||||||
|
import rlp |
||||||
|
|
||||||
|
from eth_utils.curried import ( |
||||||
|
keccak, |
||||||
|
to_int, |
||||||
|
hexstr_if_str, |
||||||
|
apply_formatters_to_dict |
||||||
|
) |
||||||
|
|
||||||
|
from rlp.sedes import ( |
||||||
|
big_endian_int, |
||||||
|
Binary, |
||||||
|
binary |
||||||
|
) |
||||||
|
|
||||||
|
from eth_account import ( |
||||||
|
Account |
||||||
|
) |
||||||
|
|
||||||
|
from eth_rlp import ( |
||||||
|
HashableRLP |
||||||
|
) |
||||||
|
|
||||||
|
from hexbytes import ( |
||||||
|
HexBytes |
||||||
|
) |
||||||
|
|
||||||
|
from eth_account._utils.signing import ( |
||||||
|
sign_transaction_hash |
||||||
|
) |
||||||
|
|
||||||
|
from eth_account._utils.transactions import ( |
||||||
|
Transaction as SignedEthereumTxData, |
||||||
|
UnsignedTransaction as UnsignedEthereumTxData, |
||||||
|
TRANSACTION_FORMATTERS as ETHEREUM_FORMATTERS, |
||||||
|
TRANSACTION_DEFAULTS, |
||||||
|
chain_id_to_v, |
||||||
|
UNSIGNED_TRANSACTION_FIELDS |
||||||
|
) |
||||||
|
|
||||||
|
from cytoolz import ( |
||||||
|
dissoc, |
||||||
|
pipe, |
||||||
|
merge, |
||||||
|
partial |
||||||
|
) |
||||||
|
|
||||||
|
from eth_account.datastructures import ( |
||||||
|
SignedTransaction |
||||||
|
) |
||||||
|
|
||||||
|
from .util import ( |
||||||
|
chain_id_to_int, |
||||||
|
convert_one_to_hex |
||||||
|
) |
||||||
|
|
||||||
|
HARMONY_FORMATTERS = dict( |
||||||
|
ETHEREUM_FORMATTERS, |
||||||
|
shardID=hexstr_if_str(to_int), # additional fields for Harmony transaction |
||||||
|
toShardID=hexstr_if_str(to_int), # which may be cross shard |
||||||
|
) |
||||||
|
|
||||||
|
class UnsignedHarmonyTxData(HashableRLP): |
||||||
|
fields = ( |
||||||
|
('nonce', big_endian_int), |
||||||
|
('gasPrice', big_endian_int), |
||||||
|
('gas', big_endian_int), |
||||||
|
('shardID', big_endian_int), |
||||||
|
('toShardID', big_endian_int), |
||||||
|
('to', Binary.fixed_length(20, allow_empty=True)), |
||||||
|
('value', big_endian_int), |
||||||
|
('data', binary), |
||||||
|
) |
||||||
|
|
||||||
|
class SignedHarmonyTxData(HashableRLP): |
||||||
|
fields = UnsignedHarmonyTxData._meta.fields + ( |
||||||
|
("v", big_endian_int), # Recovery value + 27 |
||||||
|
("r", big_endian_int), # First 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''' |
||||||
|
(v, r, s) = vrs |
||||||
|
chain_naive_transaction = dissoc( |
||||||
|
unsigned_transaction.as_dict(), 'v', 'r', 's') |
||||||
|
if isinstance(unsigned_transaction, (UnsignedHarmonyTxData, |
||||||
|
SignedHarmonyTxData)): |
||||||
|
serializer = SignedHarmonyTxData |
||||||
|
else: |
||||||
|
serializer = SignedEthereumTxData |
||||||
|
signed_transaction = serializer(v=v, r=r, s=s, **chain_naive_transaction) |
||||||
|
return rlp.encode(signed_transaction) |
||||||
|
|
||||||
|
def serialize_transaction(filled_transaction): |
||||||
|
'''serialize a signed/unsigned transaction''' |
||||||
|
if 'v' in filled_transaction: |
||||||
|
if 'shardID' in filled_transaction: |
||||||
|
serializer = SignedHarmonyTxData |
||||||
|
else: |
||||||
|
serializer = SignedEthereumTxData |
||||||
|
else: |
||||||
|
if 'shardID' in filled_transaction: |
||||||
|
serializer = UnsignedHarmonyTxData |
||||||
|
else: |
||||||
|
serializer = UnsignedEthereumTxData |
||||||
|
for f, _ in serializer._meta.fields: |
||||||
|
assert f in filled_transaction, f'Could not find {f} in transaction' |
||||||
|
return serializer.from_dict({f: filled_transaction[f] for f, _ in serializer._meta.fields}) |
||||||
|
|
||||||
|
def sanitize_transaction(transaction_dict, private_key): |
||||||
|
'''remove the originating address from the dict and convert chainId to int''' |
||||||
|
account = Account.from_key(private_key) # get account, from which you can derive public + private key |
||||||
|
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') |
||||||
|
else: |
||||||
|
raise TypeError("from field must match key's %s, but it was %s" % ( |
||||||
|
account.address, |
||||||
|
transaction_dict['from'], |
||||||
|
)) |
||||||
|
if 'chainId' in transaction_dict: |
||||||
|
transaction_dict[ 'chainId' ] = chain_id_to_int( transaction_dict[ 'chainId' ] ) |
||||||
|
return account, transaction_dict |
||||||
|
|
||||||
|
def sign_transaction(transaction_dict, private_key) -> SignedTransaction: |
||||||
|
""" |
||||||
|
Sign a (non-staking) transaction dictionary with the specified private key |
||||||
|
|
||||||
|
Parameters |
||||||
|
---------- |
||||||
|
transaction_dict: :obj:`dict` with the following keys |
||||||
|
nonce: :obj:`int` Transaction nonce |
||||||
|
gasPrice: :obj:`int` Transaction gas price in Atto |
||||||
|
gas: :obj:`int` Gas limit in Atto |
||||||
|
to: :obj:`str` Destination address |
||||||
|
value: :obj:`int` Amount to be transferred in Atto |
||||||
|
data: :obj:`str` Transaction data, used for smart contracts |
||||||
|
from: :obj:`str` From address, optional (if passed, must match the |
||||||
|
public key address generated from private_key) |
||||||
|
chainId: :obj:`int` One of util.chainIds.keys(), optional |
||||||
|
If you want to replay your transaction across networks, do not pass it |
||||||
|
shardID: :obj:`int` Originating shard ID, optional (needed for cx shard transaction) |
||||||
|
toShardID: :obj:`int` Destination shard ID, optional (needed for cx shard transaction) |
||||||
|
r: :obj:`int` First 32 bytes of the signature, optional |
||||||
|
s: :obj:`int` Next 32 bytes of the signature, optional |
||||||
|
v: :obj:`int` Recovery value, optional |
||||||
|
private_key: :obj:`str` The private key |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
A SignedTransaction object, which is a named tuple |
||||||
|
rawTransaction: :obj:`str` Hex bytes of the raw transaction |
||||||
|
hash: :obj:`str` Hex bytes of the transaction hash |
||||||
|
r: :obj:`int` First 32 bytes of the signature |
||||||
|
s: :obj:`int` Next 32 bytes of the signature |
||||||
|
v: :obj:`int` Recovery value |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
TypeError, if the from address specified is not the same |
||||||
|
one as derived from the the private key |
||||||
|
AssertionError, if the fields for the transaction are missing, |
||||||
|
or if the chainId supplied is not a string, |
||||||
|
or if the chainId is not a key in util.py |
||||||
|
|
||||||
|
API Reference |
||||||
|
------------- |
||||||
|
https://readthedocs.org/projects/eth-account/downloads/pdf/stable/ |
||||||
|
""" |
||||||
|
account, sanitized_transaction = sanitize_transaction(transaction_dict, private_key) |
||||||
|
if 'to' in sanitized_transaction and sanitized_transaction[ 'to' ] is not None: |
||||||
|
sanitized_transaction[ 'to' ] = convert_one_to_hex( sanitized_transaction[ 'to' ] ) |
||||||
|
filled_transaction = pipe( # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L39 |
||||||
|
sanitized_transaction, |
||||||
|
dict, |
||||||
|
partial(merge, TRANSACTION_DEFAULTS), |
||||||
|
chain_id_to_v, |
||||||
|
apply_formatters_to_dict(HARMONY_FORMATTERS) |
||||||
|
) |
||||||
|
unsigned_transaction = serialize_transaction(filled_transaction) |
||||||
|
transaction_hash = unsigned_transaction.hash() |
||||||
|
|
||||||
|
if isinstance(unsigned_transaction, (UnsignedEthereumTxData, UnsignedHarmonyTxData)): |
||||||
|
chain_id = None # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/signing.py#L26 |
||||||
|
else: |
||||||
|
chain_id = unsigned_transaction.v |
||||||
|
(v, r, s) = sign_transaction_hash( |
||||||
|
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( |
||||||
|
rawTransaction=HexBytes(encoded_transaction), |
||||||
|
hash=HexBytes(signed_transaction_hash), |
||||||
|
r=r, |
||||||
|
s=s, |
||||||
|
v=v, |
||||||
|
) |
@ -0,0 +1,414 @@ |
|||||||
|
from cytoolz import ( |
||||||
|
pipe, |
||||||
|
dissoc, |
||||||
|
partial, |
||||||
|
merge, |
||||||
|
identity, |
||||||
|
) |
||||||
|
|
||||||
|
from hexbytes import ( |
||||||
|
HexBytes |
||||||
|
) |
||||||
|
|
||||||
|
import rlp |
||||||
|
|
||||||
|
import math |
||||||
|
|
||||||
|
from decimal import ( |
||||||
|
Decimal |
||||||
|
) |
||||||
|
|
||||||
|
from eth_account.datastructures import ( |
||||||
|
SignedTransaction |
||||||
|
) |
||||||
|
|
||||||
|
from eth_account._utils.signing import ( |
||||||
|
sign_transaction_hash |
||||||
|
) |
||||||
|
|
||||||
|
from eth_account._utils.transactions import ( |
||||||
|
chain_id_to_v |
||||||
|
) |
||||||
|
|
||||||
|
from eth_utils.curried import ( |
||||||
|
hexstr_if_str, |
||||||
|
to_bytes, |
||||||
|
keccak, |
||||||
|
apply_formatters_to_dict, |
||||||
|
to_int, |
||||||
|
apply_formatters_to_sequence, |
||||||
|
apply_formatter_to_array |
||||||
|
) |
||||||
|
|
||||||
|
from .signing import ( |
||||||
|
sanitize_transaction |
||||||
|
) |
||||||
|
|
||||||
|
from .staking_structures import ( |
||||||
|
FORMATTERS, |
||||||
|
StakingSettings, |
||||||
|
Directive, |
||||||
|
CreateValidator, |
||||||
|
EditValidator, |
||||||
|
DelegateOrUndelegate, |
||||||
|
CollectRewards |
||||||
|
) |
||||||
|
|
||||||
|
from .util import ( |
||||||
|
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 |
||||||
|
""" |
||||||
|
Convert from staking percentage to integer |
||||||
|
For example, 0.1 becomes 1000000000000000000 |
||||||
|
|
||||||
|
Parameters |
||||||
|
--------- |
||||||
|
value: :obj:`str` or :obj:`Decimal` |
||||||
|
the value to convert |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
int, converted as above |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
AssertionError, if data types are not as expected |
||||||
|
ValueError, if the input type is not supported |
||||||
|
""" |
||||||
|
assert isinstance(value, (str, Decimal)), 'Only strings or decimals are supported' |
||||||
|
if isinstance(value, Decimal): |
||||||
|
value = str(value) |
||||||
|
value1 = value; |
||||||
|
if value[0] == '-': |
||||||
|
raise ValueError('Negative numbers are not accepted') |
||||||
|
if value[0] == '+': |
||||||
|
value1 = value[1:] |
||||||
|
if len(value1) == 0: |
||||||
|
raise ValueError('StakingDecimal string is empty') |
||||||
|
spaced = value1.split(' ') |
||||||
|
if len(spaced) > 1: |
||||||
|
raise ValueError('Bad decimal string') |
||||||
|
splitted = value1.split('.') |
||||||
|
combined_str = splitted[0] |
||||||
|
if len(splitted) == 2: |
||||||
|
length = len(splitted[1]) |
||||||
|
if length == 0 or len(combined_str) == 0: |
||||||
|
raise ValueError('Bad StakingDecimal length') |
||||||
|
if splitted[1][0] == '-': |
||||||
|
raise ValueError('Bad StakingDecimal string') |
||||||
|
combined_str += splitted[1] |
||||||
|
elif len(splitted) > 2: |
||||||
|
raise ValueError('Too many periods to be a StakingDecimal string') |
||||||
|
if length > StakingSettings.PRECISION: |
||||||
|
raise ValueError('Too much precision, must be less than {StakingSettings.PRECISION}') |
||||||
|
zeroes_to_add = StakingSettings.PRECISION - length |
||||||
|
combined_str += '0' * zeroes_to_add # This will not have any periods, so it is effectively a large integer |
||||||
|
val = int(combined_str) |
||||||
|
assert val <= StakingSettings.MAX_DECIMAL, 'Staking percentage is too large' |
||||||
|
return val |
||||||
|
|
||||||
|
def _get_account_and_transaction(transaction_dict, private_key): |
||||||
|
""" |
||||||
|
Create account from private key and sanitize the transaction |
||||||
|
Sanitization involves removal of 'from' key |
||||||
|
And conversion of chainId key from str to int (if present) |
||||||
|
|
||||||
|
Parameters |
||||||
|
---------- |
||||||
|
transaction_dict: :obj:`dict` |
||||||
|
See sign_staking_transaction |
||||||
|
private_key: obj:`str` |
||||||
|
Private key for the account |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
a tuple containing account :obj:`eth_account.Account` |
||||||
|
and sanitize_transaction :obj:`dict` |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
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 |
||||||
|
""" |
||||||
|
account, sanitized_transaction = sanitize_transaction(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 |
||||||
|
|
||||||
|
def _sign_transaction_generic(account, sanitized_transaction, parent_serializer): |
||||||
|
""" |
||||||
|
Sign a generic staking transaction, given the serializer base class and account |
||||||
|
|
||||||
|
Paramters |
||||||
|
--------- |
||||||
|
account: :obj:`eth_account.Account`, the account to use for signing |
||||||
|
sanitized_transaction: :obj:`dict`, The sanitized transaction (chainId checks and no from key) |
||||||
|
parent_serializer: :obj: The serializer class from staking_structures |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
SignedTransaction object, which can be posted to the chain by using |
||||||
|
blockchain.send_raw_transaction |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
Assertion / KeyError, if certain keys are missing from the dict |
||||||
|
rlp.exceptions.ObjectSerializationError, if data types are not as expected |
||||||
|
""" |
||||||
|
# obtain the serializers |
||||||
|
if sanitized_transaction.get('chainId', 0) == 0: |
||||||
|
unsigned_serializer, signed_serializer = parent_serializer.Unsigned(), parent_serializer.Signed() # unsigned, signed |
||||||
|
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 |
||||||
|
# fill the transaction |
||||||
|
filled_transaction = pipe( # https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L39 |
||||||
|
sanitized_transaction, |
||||||
|
dict, |
||||||
|
partial(merge, {'chainId': None}), |
||||||
|
chain_id_to_v, # will move chain id to v and add v/r/s |
||||||
|
apply_formatters_to_dict(FORMATTERS) |
||||||
|
) |
||||||
|
# get the unsigned transaction |
||||||
|
for f, _ in unsigned_serializer._meta.fields: |
||||||
|
assert f in filled_transaction, f'Could not find {f} in transaction' |
||||||
|
unsigned_transaction = unsigned_serializer.from_dict(\ |
||||||
|
{f: filled_transaction[f] for f, _ in unsigned_serializer._meta.fields}) # drop extras silently |
||||||
|
# sign the unsigned transaction |
||||||
|
if 'v' in unsigned_transaction.as_dict(): |
||||||
|
chain_id = unsigned_transaction.v |
||||||
|
else: |
||||||
|
chain_id = None |
||||||
|
transaction_hash = unsigned_transaction.hash() |
||||||
|
(v, r, s) = sign_transaction_hash( |
||||||
|
account._key_obj, transaction_hash, chain_id) |
||||||
|
chain_naive_transaction = dissoc( |
||||||
|
unsigned_transaction.as_dict(), 'v', 'r', 's') # remove extra v/r/s added by chain_id_to_v |
||||||
|
# serialize it |
||||||
|
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 |
||||||
|
r=r, |
||||||
|
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'}) |
||||||
|
# encode it |
||||||
|
encoded_transaction = rlp.encode(signed_transaction) |
||||||
|
# hash it |
||||||
|
signed_transaction_hash = keccak(encoded_transaction) |
||||||
|
# return is |
||||||
|
return SignedTransaction( |
||||||
|
rawTransaction=HexBytes(encoded_transaction), |
||||||
|
hash=HexBytes(signed_transaction_hash), |
||||||
|
r=r, |
||||||
|
s=s, |
||||||
|
v=v, |
||||||
|
) |
||||||
|
|
||||||
|
def _sign_delegate_or_undelegate(transaction_dict, private_key, delegate): |
||||||
|
""" |
||||||
|
Sign a delegate or undelegate transaction |
||||||
|
See sign_staking_transaction for details |
||||||
|
""" |
||||||
|
# preliminary steps |
||||||
|
if transaction_dict['directive'] not in [ Directive.Delegate, Directive.Undelegate ]: |
||||||
|
raise TypeError('Only Delegate or Undelegate are supported by _sign_delegate_or_undelegate') |
||||||
|
# first common step |
||||||
|
account, sanitized_transaction = _get_account_and_transaction(transaction_dict, private_key) |
||||||
|
# encode the stakeMsg |
||||||
|
sanitized_transaction['stakeMsg'] = \ |
||||||
|
apply_formatters_to_sequence( [ |
||||||
|
hexstr_if_str(to_bytes), |
||||||
|
hexstr_if_str(to_bytes), |
||||||
|
hexstr_if_str(to_int) |
||||||
|
], [ |
||||||
|
convert_one_to_hex(sanitized_transaction.pop('delegatorAddress')), |
||||||
|
convert_one_to_hex(sanitized_transaction.pop('validatorAddress')), |
||||||
|
sanitized_transaction.pop('amount'), |
||||||
|
] |
||||||
|
) |
||||||
|
return _sign_transaction_generic(account, sanitized_transaction, DelegateOrUndelegate) |
||||||
|
|
||||||
|
def _sign_collect_rewards(transaction_dict, private_key): |
||||||
|
""" |
||||||
|
Sign a collect rewards transaction |
||||||
|
See sign_staking_transaction for details |
||||||
|
""" |
||||||
|
# preliminary steps |
||||||
|
if transaction_dict['directive'] != Directive.CollectRewards: |
||||||
|
raise TypeError('Only CollectRewards is supported by _sign_collect_rewards') |
||||||
|
# first common step |
||||||
|
account, sanitized_transaction = _get_account_and_transaction(transaction_dict, private_key) |
||||||
|
# encode the stakeMsg |
||||||
|
sanitized_transaction['stakeMsg'] = \ |
||||||
|
[hexstr_if_str(to_bytes)(convert_one_to_hex(sanitized_transaction.pop('delegatorAddress')))] |
||||||
|
return _sign_transaction_generic(account, sanitized_transaction, CollectRewards) |
||||||
|
|
||||||
|
def _sign_create_validator(transaction_dict, private_key): |
||||||
|
""" |
||||||
|
Sign a create validator transaction |
||||||
|
See sign_staking_transaction for details |
||||||
|
""" |
||||||
|
# preliminary steps |
||||||
|
if transaction_dict['directive'] != Directive.CreateValidator: |
||||||
|
raise TypeError('Only CreateValidator is supported by _sign_create_or_edit_validator') |
||||||
|
# first common step |
||||||
|
account, sanitized_transaction = _get_account_and_transaction(transaction_dict, private_key) |
||||||
|
# encode the stakeMsg |
||||||
|
description = [ |
||||||
|
sanitized_transaction.pop('name'), |
||||||
|
sanitized_transaction.pop('identity'), |
||||||
|
sanitized_transaction.pop('website'), |
||||||
|
sanitized_transaction.pop('security-contact'), |
||||||
|
sanitized_transaction.pop('details'), |
||||||
|
] |
||||||
|
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('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') |
||||||
|
) |
||||||
|
sanitized_transaction['stakeMsg'] = \ |
||||||
|
apply_formatters_to_sequence( [ |
||||||
|
hexstr_if_str(to_bytes), # address |
||||||
|
identity, # description |
||||||
|
identity, # commission rates |
||||||
|
hexstr_if_str(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 |
||||||
|
hexstr_if_str(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, |
||||||
|
commission, |
||||||
|
math.floor(sanitized_transaction.pop('min-self-delegation')), # Decimal floors it correctly |
||||||
|
math.floor(sanitized_transaction.pop('max-total-delegation')), |
||||||
|
bls_keys, |
||||||
|
math.floor(sanitized_transaction.pop('amount')), |
||||||
|
] |
||||||
|
) |
||||||
|
return _sign_transaction_generic(account, sanitized_transaction, CreateValidator) |
||||||
|
|
||||||
|
def _sign_edit_validator(transaction_dict, private_key): |
||||||
|
""" |
||||||
|
Sign an edit validator transaction |
||||||
|
See sign_staking_transaction for details |
||||||
|
""" |
||||||
|
# preliminary steps |
||||||
|
if transaction_dict['directive'] != Directive.EditValidator: |
||||||
|
raise TypeError('Only EditValidator is supported by _sign_create_or_edit_validator') |
||||||
|
# first common step |
||||||
|
account, sanitized_transaction = _get_account_and_transaction(transaction_dict, private_key) |
||||||
|
# encode the stakeMsg |
||||||
|
description = [ |
||||||
|
sanitized_transaction.pop('name'), |
||||||
|
sanitized_transaction.pop('identity'), |
||||||
|
sanitized_transaction.pop('website'), |
||||||
|
sanitized_transaction.pop('security-contact'), |
||||||
|
sanitized_transaction.pop('details'), |
||||||
|
] |
||||||
|
sanitized_transaction['stakeMsg'] = \ |
||||||
|
apply_formatters_to_sequence( [ |
||||||
|
hexstr_if_str(to_bytes), # address |
||||||
|
identity, # description |
||||||
|
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(to_int), # 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 |
||||||
|
], [ |
||||||
|
convert_one_to_hex(sanitized_transaction.pop('validatorAddress')), |
||||||
|
description, |
||||||
|
[ _convert_staking_percentage_to_number(sanitized_transaction.pop('rate')) ], |
||||||
|
math.floor(sanitized_transaction.pop('min-self-delegation')), # Decimal floors it correctly |
||||||
|
math.floor(sanitized_transaction.pop('max-total-delegation')), |
||||||
|
sanitized_transaction.pop('bls-key-to-remove'), |
||||||
|
sanitized_transaction.pop('bls-key-to-add') |
||||||
|
] |
||||||
|
) |
||||||
|
return _sign_transaction_generic(account, sanitized_transaction, EditValidator) |
||||||
|
|
||||||
|
def sign_staking_transaction(transaction_dict, private_key): |
||||||
|
""" |
||||||
|
Sign a supplied transaction_dict with the private_key |
||||||
|
|
||||||
|
Parameters |
||||||
|
---------- |
||||||
|
transaction_dict: :obj:`dict`, a dictionary with the following keys |
||||||
|
directive :obj:`staking_structures.Directive`, type of transaction |
||||||
|
nonce: :obj:`int`, nonce of transaction |
||||||
|
gasPrice: :obj:`int`, gas price for the transaction |
||||||
|
gasLimit: :obj:`int`, gas limit for the transaction |
||||||
|
chainId: :obj:`int`, chain id for the transaction, optional |
||||||
|
see util.chain_id_to_int for options |
||||||
|
The following keys depend on the directive: |
||||||
|
CollectRewards: |
||||||
|
delegatorAddress: :obj:`str`, Address of the delegator |
||||||
|
Delegate/Undelegate: |
||||||
|
delegatorAddress: :obj:`str`, Address of the delegator |
||||||
|
validatorAddress: :obj:`str`, Address of the validator |
||||||
|
amount: :obj:`int`, Amount to (un)delegate in ATTO |
||||||
|
CreateValidator: |
||||||
|
validatorAddress: :obj:`str`, Address of the validator |
||||||
|
name: ;obj:`str`, Name of the validator |
||||||
|
identity: :obj:`str`, Identity of the validator, must be unique |
||||||
|
website: :obj:`str`, Website of the validator |
||||||
|
security-contact: :obj:`str`, Security contact |
||||||
|
details: :obj:`str` Validator details |
||||||
|
rate: :obj:'Decimal' or :obj:`str` Staking commission rate |
||||||
|
max-rate: :obj:'Decimal' or :obj:`str` Maximum staking commission rate |
||||||
|
max-change-rate: :obj:'Decimal' or :obj:`str` Maximum change in |
||||||
|
staking commission rate per epoch |
||||||
|
bls-public-keys: :obj:`list` List of strings of BLS public keys |
||||||
|
min-self-delegation: :obj:`int` or :obj:`Decimal` Validator min |
||||||
|
self delegation in ATTO |
||||||
|
max-total-delegation: :obj:`int` or :obj:`Decimal` Validator max |
||||||
|
total delegation in ATTO |
||||||
|
EditValidator: |
||||||
|
validatorAddress: :obj:`str`, Address of the validator |
||||||
|
name: ;obj:`str`, Name of the validator |
||||||
|
identity: :obj:`str`, Identity of the validator, must be unique |
||||||
|
website: :obj:`str`, Website of the validator |
||||||
|
security-contact: :obj:`str`, Security contact |
||||||
|
details: :obj:`str` Validator details |
||||||
|
rate: :obj:'Decimal' or :obj:`str` Staking commission rate |
||||||
|
min-self-delegation: :obj:`int` or :obj:`Decimal` Validator min |
||||||
|
self delegation in ATTO |
||||||
|
max-total-delegation: :obj:`int` or :obj:`Decimal` Validator max |
||||||
|
total delegation in ATTO |
||||||
|
bls-key-to-remove: :obj:`str` BLS Public key to remove |
||||||
|
bls-key-to-add: :obj:`str` BLS Public key to add |
||||||
|
private_key: :obj:`str`, the private key to sign the transaction with |
||||||
|
|
||||||
|
Raises |
||||||
|
------ |
||||||
|
AssertionError, if inputs are not as expected |
||||||
|
KeyError, if inputs are missing |
||||||
|
ValueError, if specifically staking rates are malformed |
||||||
|
rlp.exceptions.ObjectSerializationError, if input data types are not as expected |
||||||
|
|
||||||
|
Returns |
||||||
|
------- |
||||||
|
SignedTransaction object, the hash of which can be used to send the transaction |
||||||
|
using transaction.send_raw_transaction |
||||||
|
|
||||||
|
API Reference |
||||||
|
------------- |
||||||
|
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 'directive' in transaction_dict, 'Staking transaction type not specified' |
||||||
|
assert isinstance(transaction_dict['directive'], Directive), 'Unknown staking transaction type' |
||||||
|
if transaction_dict['directive'] == Directive.CollectRewards: |
||||||
|
return _sign_collect_rewards(transaction_dict, private_key) |
||||||
|
elif transaction_dict['directive'] == Directive.Delegate: |
||||||
|
return _sign_delegate_or_undelegate(transaction_dict, private_key, True) |
||||||
|
elif transaction_dict['directive'] == Directive.Undelegate: |
||||||
|
return _sign_delegate_or_undelegate(transaction_dict, private_key, False) |
||||||
|
elif transaction_dict['directive'] == Directive.CreateValidator: |
||||||
|
return _sign_create_validator(transaction_dict, private_key) |
||||||
|
elif transaction_dict['directive'] == Directive.EditValidator: |
||||||
|
return _sign_edit_validator(transaction_dict, private_key) |
@ -0,0 +1,218 @@ |
|||||||
|
from enum import ( |
||||||
|
Enum, |
||||||
|
auto |
||||||
|
) |
||||||
|
|
||||||
|
from rlp.sedes import ( |
||||||
|
big_endian_int, |
||||||
|
Binary, |
||||||
|
CountableList, |
||||||
|
List, |
||||||
|
Text |
||||||
|
) |
||||||
|
|
||||||
|
from eth_rlp import ( |
||||||
|
HashableRLP |
||||||
|
) |
||||||
|
|
||||||
|
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 |
||||||
|
CreateValidator = auto() |
||||||
|
EditValidator = auto() |
||||||
|
Delegate = auto() |
||||||
|
Undelegate = auto() |
||||||
|
CollectRewards = auto() |
||||||
|
|
||||||
|
FORMATTERS = { |
||||||
|
'directive': hexstr_if_str(to_int), # delegatorAddress is already formatted before the call |
||||||
|
'nonce': 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: |
||||||
|
@staticmethod |
||||||
|
def UnsignedChainId(): |
||||||
|
class UnsignedChainId(HashableRLP): |
||||||
|
fields = ( |
||||||
|
('directive', big_endian_int), |
||||||
|
('stakeMsg', CountableList(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 |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def SignedChainId(): |
||||||
|
class SignedChainId(HashableRLP): |
||||||
|
fields = CollectRewards.UnsignedChainId()._meta.fields[:-1] + ( # drop chainId |
||||||
|
("v", big_endian_int), |
||||||
|
("r", big_endian_int), |
||||||
|
("s", big_endian_int), |
||||||
|
) |
||||||
|
return SignedChainId |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def Unsigned(): |
||||||
|
class Unsigned(HashableRLP): |
||||||
|
fields = CollectRewards.UnsignedChainId()._meta.fields[:-1] # drop chainId |
||||||
|
return Unsigned |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def Signed(): |
||||||
|
class Signed(HashableRLP): |
||||||
|
fields = CollectRewards.Unsigned()._meta.fields[:-3] + ( # drop last 3 for raw.pop() |
||||||
|
("v", big_endian_int), |
||||||
|
("r", big_endian_int), |
||||||
|
("s", big_endian_int), |
||||||
|
) |
||||||
|
return Signed |
||||||
|
|
||||||
|
class DelegateOrUndelegate: |
||||||
|
@staticmethod |
||||||
|
def UnsignedChainId(): |
||||||
|
class UnsignedChainId(HashableRLP): |
||||||
|
fields = ( |
||||||
|
('directive', big_endian_int), |
||||||
|
('stakeMsg', List([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 |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def SignedChainId(): |
||||||
|
class SignedChainId(HashableRLP): |
||||||
|
fields = DelegateOrUndelegate.UnsignedChainId()._meta.fields[:-1] + ( # drop chainId |
||||||
|
("v", big_endian_int), |
||||||
|
("r", big_endian_int), |
||||||
|
("s", big_endian_int), |
||||||
|
) |
||||||
|
return SignedChainId |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def Unsigned(): |
||||||
|
class Unsigned(HashableRLP): |
||||||
|
fields = DelegateOrUndelegate.UnsignedChainId()._meta.fields[:-1] # drop chainId |
||||||
|
return Unsigned |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def Signed(): |
||||||
|
class Signed(HashableRLP): |
||||||
|
fields = DelegateOrUndelegate.Unsigned()._meta.fields[:-3] + ( # drop last 3 for raw.pop() |
||||||
|
("v", big_endian_int), |
||||||
|
("r", big_endian_int), |
||||||
|
("s", big_endian_int), |
||||||
|
) |
||||||
|
return Signed |
||||||
|
|
||||||
|
class CreateValidator: |
||||||
|
@staticmethod |
||||||
|
def UnsignedChainId(): |
||||||
|
class UnsignedChainId(HashableRLP): |
||||||
|
fields = ( |
||||||
|
('directive', big_endian_int), |
||||||
|
('stakeMsg', List([ # list with the following members |
||||||
|
Binary.fixed_length(20, allow_empty=True), # validatorAddress |
||||||
|
List([Text()]*5,True), # description is Text of 5 elements |
||||||
|
List([List([big_endian_int],True)]*3,True), # commission rate is made up of 3 integers in an array [ [int1], [int2], [int3] ] |
||||||
|
big_endian_int, # min self 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 |
||||||
|
big_endian_int, # amount |
||||||
|
], True)), # strictly these number of elements |
||||||
|
('nonce', big_endian_int), |
||||||
|
('gasPrice', big_endian_int), |
||||||
|
('gasLimit', big_endian_int), |
||||||
|
('chainId', big_endian_int), |
||||||
|
) |
||||||
|
return UnsignedChainId |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def SignedChainId(): |
||||||
|
class SignedChainId(HashableRLP): |
||||||
|
fields = CreateValidator.UnsignedChainId()._meta.fields[:-1] + ( # drop chainId |
||||||
|
("v", big_endian_int), |
||||||
|
("r", big_endian_int), |
||||||
|
("s", big_endian_int), |
||||||
|
) |
||||||
|
return SignedChainId |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def Unsigned(): |
||||||
|
class Unsigned(HashableRLP): |
||||||
|
fields = CreateValidator.UnsignedChainId()._meta.fields[:-1] # drop chainId |
||||||
|
return Unsigned |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def Signed(): |
||||||
|
class Signed(HashableRLP): |
||||||
|
fields = CreateValidator.Unsigned()._meta.fields[:-3] + ( # drop last 3 for raw.pop() |
||||||
|
("v", big_endian_int), |
||||||
|
("r", big_endian_int), |
||||||
|
("s", big_endian_int), |
||||||
|
) |
||||||
|
return Signed |
||||||
|
|
||||||
|
class EditValidator: |
||||||
|
@staticmethod |
||||||
|
def UnsignedChainId(): |
||||||
|
class UnsignedChainId(HashableRLP): |
||||||
|
fields = ( |
||||||
|
('directive', big_endian_int), |
||||||
|
('stakeMsg', List([ # list with the following members |
||||||
|
Binary.fixed_length(20, allow_empty=True), # validatorAddress |
||||||
|
List([Text()]*5,True), # description is Text of 5 elements |
||||||
|
List([big_endian_int],True), # new rate is in a list |
||||||
|
big_endian_int, # min self delegation |
||||||
|
big_endian_int, # max total delegation |
||||||
|
Binary.fixed_length(48, allow_empty=True), # slot key to remove |
||||||
|
Binary.fixed_length(48, allow_empty=True), # slot key to add |
||||||
|
], True)), # strictly these number of elements |
||||||
|
('nonce', big_endian_int), |
||||||
|
('gasPrice', big_endian_int), |
||||||
|
('gasLimit', big_endian_int), |
||||||
|
('chainId', big_endian_int), |
||||||
|
) |
||||||
|
return UnsignedChainId |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def SignedChainId(): |
||||||
|
class SignedChainId(HashableRLP): |
||||||
|
fields = EditValidator.UnsignedChainId()._meta.fields[:-1] + ( # drop chainId |
||||||
|
("v", big_endian_int), |
||||||
|
("r", big_endian_int), |
||||||
|
("s", big_endian_int), |
||||||
|
) |
||||||
|
return SignedChainId |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def Unsigned(): |
||||||
|
class Unsigned(HashableRLP): |
||||||
|
fields = EditValidator.UnsignedChainId()._meta.fields[:-1] # drop chainId |
||||||
|
return Unsigned |
||||||
|
|
||||||
|
@staticmethod |
||||||
|
def Signed(): |
||||||
|
class Signed(HashableRLP): |
||||||
|
fields = EditValidator.Unsigned()._meta.fields[:-3] + ( # drop last 3 for raw.pop() |
||||||
|
("v", big_endian_int), |
||||||
|
("r", big_endian_int), |
||||||
|
("s", big_endian_int), |
||||||
|
) |
||||||
|
return Signed |
@ -0,0 +1,9 @@ |
|||||||
|
from pyhmy.bech32 import ( |
||||||
|
bech32 |
||||||
|
) |
||||||
|
|
||||||
|
def test_encode(): |
||||||
|
bech32.encode('one', 5, [121, 161]) |
||||||
|
|
||||||
|
def test_decode(): |
||||||
|
bech32.decode('one', 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9') |
@ -0,0 +1,74 @@ |
|||||||
|
import pytest |
||||||
|
|
||||||
|
from pyhmy import ( |
||||||
|
contract |
||||||
|
) |
||||||
|
|
||||||
|
from pyhmy.rpc import ( |
||||||
|
exceptions |
||||||
|
) |
||||||
|
|
||||||
|
explorer_endpoint = 'http://localhost:9599' |
||||||
|
contract_tx_hash = '0xa13414dd152173395c69a11e79dea31bf029660f747a42a53744181d05571e70' |
||||||
|
contract_address = None |
||||||
|
fake_shard = 'http://example.com' |
||||||
|
|
||||||
|
def _test_contract_rpc(fn, *args, **kwargs): |
||||||
|
if not callable(fn): |
||||||
|
pytest.fail(f'Invalid function: {fn}') |
||||||
|
|
||||||
|
try: |
||||||
|
response = fn(*args, **kwargs) |
||||||
|
except Exception as e: |
||||||
|
if isinstance(e, exceptions.RPCError) and 'does not exist/is not available' in str(e): |
||||||
|
pytest.skip(f'{str(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 |
||||||
|
|
||||||
|
@pytest.mark.run(order=1) |
||||||
|
def test_get_contract_address_from_hash(setup_blockchain): |
||||||
|
global contract_address |
||||||
|
contract_address = _test_contract_rpc(contract.get_contract_address_from_hash, contract_tx_hash) |
||||||
|
assert isinstance(contract_address, str) |
||||||
|
|
||||||
|
@pytest.mark.run(order=2) |
||||||
|
def test_call(setup_blockchain): |
||||||
|
if not contract_address: |
||||||
|
pytest.skip('Contract address not loaded yet') |
||||||
|
called = _test_contract_rpc(contract.call, contract_address, 'latest') |
||||||
|
assert isinstance(called, str) and called.startswith('0x') |
||||||
|
|
||||||
|
@pytest.mark.run(order=3) |
||||||
|
def test_estimate_gas(setup_blockchain): |
||||||
|
if not contract_address: |
||||||
|
pytest.skip('Contract address not loaded yet') |
||||||
|
gas = _test_contract_rpc(contract.estimate_gas, contract_address) |
||||||
|
assert isinstance(gas, int) |
||||||
|
|
||||||
|
@pytest.mark.run(order=4) |
||||||
|
def test_get_code(setup_blockchain): |
||||||
|
if not contract_address: |
||||||
|
pytest.skip('Contract address not loaded yet') |
||||||
|
code = _test_contract_rpc(contract.get_code, contract_address, 'latest') |
||||||
|
assert code == '0x608060405234801561001057600080fd5b50600436106100415760003560e01c8063445df0ac146100465780638da5cb5b14610064578063fdacd576146100ae575b600080fd5b61004e6100dc565b6040518082815260200191505060405180910390f35b61006c6100e2565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100da600480360360208110156100c457600080fd5b8101908080359060200190929190505050610107565b005b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561016457806001819055505b5056fea265627a7a723158209b80813a158b44af65aee232b44c0ac06472c48f4abbe298852a39f0ff34a9f264736f6c63430005100032' |
||||||
|
|
||||||
|
@pytest.mark.run(order=5) |
||||||
|
def test_get_storage_at(setup_blockchain): |
||||||
|
if not contract_address: |
||||||
|
pytest.skip('Contract address not loaded yet') |
||||||
|
storage = _test_contract_rpc(contract.get_storage_at, contract_address, '0x0', 'latest') |
||||||
|
assert isinstance(storage, str) and storage.startswith('0x') |
||||||
|
|
||||||
|
def test_errors(): |
||||||
|
with pytest.raises(exceptions.RPCError): |
||||||
|
contract.get_contract_address_from_hash('', fake_shard) |
||||||
|
with pytest.raises(exceptions.RPCError): |
||||||
|
contract.call('', '', endpoint=fake_shard) |
||||||
|
with pytest.raises(exceptions.RPCError): |
||||||
|
contract.estimate_gas('', endpoint=fake_shard) |
||||||
|
with pytest.raises(exceptions.RPCError): |
||||||
|
contract.get_code('', 'latest', endpoint=fake_shard) |
||||||
|
with pytest.raises(exceptions.RPCError): |
||||||
|
contract.get_storage_at('', 1, 'latest', endpoint=fake_shard) |
@ -0,0 +1,86 @@ |
|||||||
|
from pyhmy import ( |
||||||
|
signing |
||||||
|
) |
||||||
|
|
||||||
|
""" |
||||||
|
Test signature source (node.js) |
||||||
|
import { Transaction, RLPSign, TxStatus } from '@harmony-js/transaction'; |
||||||
|
import { HttpProvider, Messenger } from '@harmony-js/network'; |
||||||
|
import { ChainType, ChainID } from '@harmony-js/utils'; |
||||||
|
|
||||||
|
const provider = new HttpProvider('http://localhost:9500'); |
||||||
|
let privateKey = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48' |
||||||
|
let hmyMessenger = new Messenger(provider, ChainType.Ethereum, ChainID.Default); |
||||||
|
|
||||||
|
let transaction: Transaction = new Transaction( |
||||||
|
{ |
||||||
|
gasLimit: 100, |
||||||
|
gasPrice: 1, |
||||||
|
to: "one1z3u3d9expexf5u03sjzvn7vhkvywtye9nqmmlu", |
||||||
|
value: 5, |
||||||
|
nonce: 2, |
||||||
|
}, |
||||||
|
hmyMessenger, |
||||||
|
TxStatus.INTIALIZED, |
||||||
|
); |
||||||
|
console.log('Unsigned transaction') |
||||||
|
let payload = transaction.txPayload |
||||||
|
console.log(payload) |
||||||
|
let signed = RLPSign(transaction, privateKey); |
||||||
|
console.log( 'Signed transaction' ) |
||||||
|
console.log(signed) |
||||||
|
""" |
||||||
|
def test_eth_transaction(): |
||||||
|
transaction_dict = { |
||||||
|
'nonce': 2, |
||||||
|
'gasPrice': 1, |
||||||
|
'gas': 100, # signing.py uses Ether, which by default calls it gas |
||||||
|
'to': '0x14791697260e4c9a71f18484c9f997b308e59325', |
||||||
|
'value': 5, |
||||||
|
} |
||||||
|
signed_tx = signing.sign_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
assert signed_tx.rawTransaction.hex() == '0xf85d0201649414791697260e4c9a71f18484c9f997b308e5932505801ca0b364f4296bfd3231889d1b9ac94c68abbcb8ee6a6c7a5fa412ac82b5b7b0d5d1a02233864842ab28ee4f99c207940a867b0f8534ca362836190792816b48dde3b1' |
||||||
|
|
||||||
|
""" |
||||||
|
Test signature source (node.js) |
||||||
|
import { Transaction, RLPSign, TxStatus } from '@harmony-js/transaction'; |
||||||
|
import { HttpProvider, Messenger } from '@harmony-js/network'; |
||||||
|
import { ChainType, ChainID } from '@harmony-js/utils'; |
||||||
|
|
||||||
|
const provider = new HttpProvider('http://localhost:9500'); |
||||||
|
let privateKey = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48' |
||||||
|
let hmyMessenger = new Messenger(provider, ChainType.Harmony, ChainID.HmyMainnet); |
||||||
|
|
||||||
|
let transaction: Transaction = new Transaction( |
||||||
|
{ |
||||||
|
gasLimit: 100, |
||||||
|
gasPrice: 1, |
||||||
|
to: "one1z3u3d9expexf5u03sjzvn7vhkvywtye9nqmmlu", |
||||||
|
value: 5, |
||||||
|
nonce: 2, |
||||||
|
shardID: 0, |
||||||
|
toShardID: 1 |
||||||
|
}, |
||||||
|
hmyMessenger, |
||||||
|
TxStatus.INTIALIZED, |
||||||
|
); |
||||||
|
console.log('Unsigned transaction') |
||||||
|
let payload = transaction.txPayload |
||||||
|
console.log(payload) |
||||||
|
let signed = RLPSign(transaction, privateKey); |
||||||
|
console.log( 'Signed transaction' ) |
||||||
|
console.log(signed) |
||||||
|
""" |
||||||
|
def test_hmy_transaction(): |
||||||
|
transaction_dict = { |
||||||
|
'nonce': 2, |
||||||
|
'gasPrice': 1, |
||||||
|
'gas': 100, # signing.py uses Ether, which by default calls it gas |
||||||
|
'to': '0x14791697260e4c9a71f18484c9f997b308e59325', |
||||||
|
'value': 5, |
||||||
|
'shardID': 0, |
||||||
|
'toShardID': 1, |
||||||
|
'chainId': 'HmyMainnet' |
||||||
|
} |
||||||
|
signed_tx = signing.sign_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
assert signed_tx.rawTransaction.hex() == '0xf85f02016480019414791697260e4c9a71f18484c9f997b308e59325058026a02a203357ca6d7cdec981ad3d3692ad2c9e24536a9b6e7b486ce2f94f28c7563ea010d38cd0312a153af0aa7d8cd986040c36118bba373cb94e3e86fd4aedce904d' |
@ -0,0 +1,100 @@ |
|||||||
|
from pyhmy import ( |
||||||
|
staking_signing, |
||||||
|
staking_structures |
||||||
|
) |
||||||
|
|
||||||
|
from pyhmy.numbers import ( |
||||||
|
convert_one_to_atto |
||||||
|
) |
||||||
|
|
||||||
|
# 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 |
||||||
|
|
||||||
|
""" |
||||||
|
let stakingTx |
||||||
|
let stakeMsg3: CollectRewards = new CollectRewards( |
||||||
|
'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9' |
||||||
|
) |
||||||
|
stakingTx = new StakingTransaction( |
||||||
|
Directive.DirectiveCollectRewards, |
||||||
|
stakeMsg3, |
||||||
|
2, // nonce |
||||||
|
numberToHex(new Unit('1').asOne().toWei()), // gasPrice |
||||||
|
100, // gasLimit |
||||||
|
null, // chainId |
||||||
|
); |
||||||
|
const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
console.log( 'Signed transaction' ) |
||||||
|
console.log(signed) |
||||||
|
""" |
||||||
|
def test_collect_rewards_no_chain_id(): |
||||||
|
transaction_dict = { |
||||||
|
'directive': staking_structures.Directive.CollectRewards, |
||||||
|
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', |
||||||
|
'nonce': 2, |
||||||
|
'gasPrice': int(convert_one_to_atto(1)), |
||||||
|
'gasLimit': 100, |
||||||
|
} |
||||||
|
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
assert signed_tx.rawTransaction.hex() == '0xf85a04d594ebcd16e8c1d8f493ba04e99a56474122d81a9c5823a0490e4ceb747563ba40da3e0db8a65133cf6f6ae4c48a24866cd6aa1f0d6c2414a06dbd51a67b35b5685e7b7420cba26e63b0e7d3c696fc6cb69d48e54fcad280e9' |
||||||
|
|
||||||
|
""" |
||||||
|
let stakingTx |
||||||
|
let stakeMsg3: CollectRewards = new CollectRewards( |
||||||
|
'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9' |
||||||
|
) |
||||||
|
stakingTx = new StakingTransaction( |
||||||
|
Directive.DirectiveCollectRewards, |
||||||
|
stakeMsg3, |
||||||
|
2, // nonce |
||||||
|
numberToHex(new Unit('1').asOne().toWei()), // gasPrice |
||||||
|
100, // gasLimit |
||||||
|
1, // chainId |
||||||
|
); |
||||||
|
const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
console.log( 'Signed transaction' ) |
||||||
|
console.log(signed) |
||||||
|
""" |
||||||
|
def test_collect_rewards_chain_id(): |
||||||
|
transaction_dict = { |
||||||
|
'directive': staking_structures.Directive.CollectRewards, |
||||||
|
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', |
||||||
|
'nonce': 2, |
||||||
|
'gasPrice': int(convert_one_to_atto(1)), |
||||||
|
'gasLimit': 100, |
||||||
|
'chainId': 1, # with chainId for coverage |
||||||
|
} |
||||||
|
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
assert signed_tx.rawTransaction.hex() == '0xf86504d594ebcd16e8c1d8f493ba04e99a56474122d81a9c5802880de0b6b3a76400006425a055d6c3c0d8e7a1e75152db361a2ed47f5ab54f6f19b0d8e549953dbdf13ba647a076e1367dfca38eae3bd0e8da296335acabbaeb87dc17e47ebe4942db29334099' |
||||||
|
|
||||||
|
""" |
||||||
|
let stakingTx |
||||||
|
let stakeMsg4: Delegate = new Delegate( |
||||||
|
'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', |
||||||
|
'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', |
||||||
|
5 |
||||||
|
) |
||||||
|
stakingTx = new StakingTransaction( |
||||||
|
Directive.DirectiveDelegate, |
||||||
|
stakeMsg4, |
||||||
|
2, // nonce |
||||||
|
numberToHex(new Unit('1').asOne().toWei()), // gasPrice |
||||||
|
100, // gasLimit |
||||||
|
null, // chainId |
||||||
|
); |
||||||
|
const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
console.log( 'Signed transaction' ) |
||||||
|
console.log(signed) |
||||||
|
""" |
||||||
|
def test_delegate(): |
||||||
|
transaction_dict = { |
||||||
|
'directive': staking_structures.Directive.Delegate, |
||||||
|
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', |
||||||
|
'validatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', |
||||||
|
'amount': 5, |
||||||
|
'nonce': 2, |
||||||
|
'gasPrice': int(convert_one_to_atto(1)), |
||||||
|
'gasLimit': 100, |
||||||
|
} |
||||||
|
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
assert signed_tx.rawTransaction.hex() == '0xf87002eb94ebcd16e8c1d8f493ba04e99a56474122d81a9c5894ebcd16e8c1d8f493ba04e99a56474122d81a9c580523a0aceff4166ec0ecd0cc664fed865270fe77b35e408138950f802129f1f3d06a74a06f9aca402fb6b4842bff8d65f430d82eefa95645e9046b102195d1044993f9fe' |
@ -0,0 +1,205 @@ |
|||||||
|
import pytest |
||||||
|
import requests |
||||||
|
from decimal import ( |
||||||
|
Decimal |
||||||
|
) |
||||||
|
|
||||||
|
from pyhmy import ( |
||||||
|
validator |
||||||
|
) |
||||||
|
|
||||||
|
from pyhmy.rpc import ( |
||||||
|
exceptions |
||||||
|
) |
||||||
|
|
||||||
|
from pyhmy.numbers import ( |
||||||
|
convert_one_to_atto |
||||||
|
) |
||||||
|
|
||||||
|
from pyhmy.exceptions import ( |
||||||
|
InvalidValidatorError |
||||||
|
) |
||||||
|
|
||||||
|
import sys |
||||||
|
|
||||||
|
test_epoch_number = 0 |
||||||
|
genesis_block_number = 0 |
||||||
|
test_block_number = 1 |
||||||
|
test_validator_object = None |
||||||
|
test_validator_loaded = False |
||||||
|
|
||||||
|
@pytest.mark.run(order=0) |
||||||
|
def test_instantiate_validator(setup_blockchain): |
||||||
|
global test_validator_object |
||||||
|
test_validator_object = validator.Validator('one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9') |
||||||
|
assert isinstance(test_validator_object, validator.Validator) |
||||||
|
|
||||||
|
@pytest.mark.run(order=1) |
||||||
|
def test_load_validator(setup_blockchain): |
||||||
|
if not test_validator_object: |
||||||
|
pytest.skip('Validator not instantiated yet') |
||||||
|
info = { |
||||||
|
'name': 'Alice', |
||||||
|
'identity': 'alice', |
||||||
|
'website': 'alice.harmony.one', |
||||||
|
'details': "Don't mess with me!!!", |
||||||
|
'security-contact': 'Bob', |
||||||
|
'min-self-delegation': convert_one_to_atto(10000), |
||||||
|
'amount': convert_one_to_atto(10001), |
||||||
|
'max-rate': '0.9', |
||||||
|
'max-change-rate': '0.05', |
||||||
|
'rate': '0.01', |
||||||
|
'bls-public-keys': ['0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611'], |
||||||
|
'max-total-delegation': convert_one_to_atto(40000) |
||||||
|
} |
||||||
|
test_validator_object.load(info) |
||||||
|
global test_validator_loaded |
||||||
|
test_validator_loaded = True |
||||||
|
|
||||||
|
""" |
||||||
|
TypeScript signature source |
||||||
|
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 stakeMsg: CreateValidator = new CreateValidator( |
||||||
|
'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', |
||||||
|
description, |
||||||
|
commissionRates, |
||||||
|
numberToHex(new Unit('10000').asOne().toWei()), // minSelfDelegation |
||||||
|
numberToHex(new Unit('40000').asOne().toWei()), // maxTotalDelegation |
||||||
|
[ '0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611' ], |
||||||
|
numberToHex(new Unit('10001').asOne().toWei()) // amount |
||||||
|
) |
||||||
|
const stakingTx: StakingTransaction = new StakingTransaction( |
||||||
|
Directive.DirectiveCreateValidator, |
||||||
|
stakeMsg, |
||||||
|
2, // nonce |
||||||
|
numberToHex(new Unit('1').asOne().toWei()), // gasPrice |
||||||
|
100, // gasLimit |
||||||
|
null, // chainId |
||||||
|
); |
||||||
|
const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
console.log( 'Signed transaction' ) |
||||||
|
console.log(signed) |
||||||
|
""" |
||||||
|
@pytest.mark.run(order=2) |
||||||
|
def test_create_validator_sign(setup_blockchain): |
||||||
|
if not (test_validator_object or test_validator_loaded): |
||||||
|
pytest.skip('Validator not ready yet') |
||||||
|
signed_hash = test_validator_object.sign_create_validator_transaction( |
||||||
|
2, |
||||||
|
int(convert_one_to_atto(1)), |
||||||
|
100, |
||||||
|
'4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48', |
||||||
|
None).rawTransaction.hex() |
||||||
|
assert signed_hash == '0xf9010580f8bf94ebcd16e8c1d8f493ba04e99a56474122d81a9c58f83885416c69636585616c69636591616c6963652e6861726d6f6e792e6f6e6583426f6295446f6e2774206d6573732077697468206d65212121dcc8872386f26fc10000c9880c7d713b49da0000c887b1a2bc2ec500008a021e19e0c9bab24000008a0878678326eac9000000f1b0b9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b622476086118a021e27c1806e59a4000024a047c6d444971d4d3c48e8b255aa0e543ebb47b60f761582694e5af5330445aba5a04db1ffea9cca9f9e56e8f782c689db680992903acfd9c06f4593f7fd9a781bd7' |
||||||
|
|
||||||
|
""" |
||||||
|
Signature matched from TypeScript |
||||||
|
import { |
||||||
|
CreateValidator, |
||||||
|
EditValidator, |
||||||
|
Delegate, |
||||||
|
Undelegate, |
||||||
|
CollectRewards, |
||||||
|
Directive, |
||||||
|
Description, |
||||||
|
CommissionRate, |
||||||
|
Decimal, |
||||||
|
StakingTransaction, |
||||||
|
} from '@harmony-js/staking' |
||||||
|
const { numberToHex, Unit } = require('@harmony-js/utils'); |
||||||
|
|
||||||
|
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 stakeMsg: EditValidator = new EditValidator( |
||||||
|
'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9', |
||||||
|
description, |
||||||
|
new Decimal('0.06'), |
||||||
|
numberToHex(new Unit('10000').asOne().toWei()), // minSelfDelegation |
||||||
|
numberToHex(new Unit('40000').asOne().toWei()), // maxTotalDelegation |
||||||
|
'0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611', // remove key |
||||||
|
'0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608612' // add key |
||||||
|
) |
||||||
|
const stakingTx: StakingTransaction = new StakingTransaction( |
||||||
|
Directive.DirectiveEditValidator, |
||||||
|
stakeMsg, |
||||||
|
2, // nonce |
||||||
|
numberToHex(new Unit('1').asOne().toWei()), // gasPrice |
||||||
|
100, // gasLimit |
||||||
|
2, // chainId |
||||||
|
); |
||||||
|
const signed = stakingTx.rlpSign('4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48') |
||||||
|
console.log( 'Signed transaction' ) |
||||||
|
console.log(signed) |
||||||
|
""" |
||||||
|
@pytest.mark.run(order=3) |
||||||
|
def test_edit_validator_sign(setup_blockchain): |
||||||
|
if not (test_validator_object or test_validator_loaded): |
||||||
|
pytest.skip('Validator not ready yet') |
||||||
|
signed_hash = test_validator_object.sign_edit_validator_transaction( |
||||||
|
2, |
||||||
|
int(convert_one_to_atto(1)), |
||||||
|
100, |
||||||
|
'0.06', |
||||||
|
'0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608612', # add key |
||||||
|
"0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611", # remove key |
||||||
|
'4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48', |
||||||
|
2).rawTransaction.hex() |
||||||
|
assert signed_hash == '0xf9012101f8d094ebcd16e8c1d8f493ba04e99a56474122d81a9c58f83885416c69636585616c69636591616c6963652e6861726d6f6e792e6f6e6583426f6295446f6e2774206d6573732077697468206d65212121c887d529ae9e8600008a021e19e0c9bab24000008a0878678326eac9000000b0b9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611b0b9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b6224760861202880de0b6b3a76400006428a0656d6741687ec1e42d1699274584a1777964e939b0ef11f3ff0e161859da21a2a03fc51e067f9fb6c96bee5ceccad4104f5b4b334a86a36a2f53d10b9a8e4a268a' |
||||||
|
|
||||||
|
@pytest.mark.run(order=4) |
||||||
|
def test_invalid_validator(setup_blockchain): |
||||||
|
if not (test_validator_object or test_validator_loaded): |
||||||
|
pytest.skip('Validator not ready yet') |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
info = { |
||||||
|
'name': 'Alice', |
||||||
|
} |
||||||
|
test_validator_object.load(info) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_name('a'*141) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_identity('a'*141) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_website('a'*141) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_security_contact('a'*141) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_details('a'*281) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_min_self_delegation(1) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_max_total_delegation(1) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_amount(1) |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_max_rate('2.0') |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_max_change_rate('-2.0') |
||||||
|
with pytest.raises(InvalidValidatorError): |
||||||
|
test_validator_object.set_rate('-2.0') |
||||||
|
|
||||||
|
@pytest.mark.run(order=5) |
||||||
|
def test_validator_getters(setup_blockchain): |
||||||
|
if not (test_validator_object or test_validator_loaded): |
||||||
|
pytest.skip('Validator not ready yet') |
||||||
|
assert test_validator_object.get_address() == 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9' |
||||||
|
assert test_validator_object.add_bls_key('5') |
||||||
|
assert test_validator_object.remove_bls_key('5') |
||||||
|
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 isinstance(test_validator_object.get_min_self_delegation(), Decimal) |
||||||
|
assert isinstance(test_validator_object.get_max_total_delegation(), Decimal) |
||||||
|
assert isinstance(test_validator_object.get_amount(), Decimal) |
||||||
|
assert isinstance(test_validator_object.get_max_rate(), Decimal) |
||||||
|
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 |
||||||
|
|
||||||
|
@pytest.mark.run(order=6) |
||||||
|
def test_validator_load_from_blockchain(setup_blockchain): |
||||||
|
test_validator_object2 = validator.Validator('one109r0tns7av5sjew7a7fkekg4fs3pw0h76pp45e') |
||||||
|
test_validator_object2.load_from_blockchain() |
Loading…
Reference in new issue