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