Merge pull request #20 from MaxMustermann2/master

Resolve Bounty 7 (improve Harmony Python SDK)
pull/22/head
Ganesha Upadhyaya 3 years ago committed by GitHub
commit 099274bec1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 545
      README.md
  2. 220
      pyhmy/account.py
  3. 1309
      pyhmy/blockchain.py
  4. 240
      pyhmy/contract.py
  5. 9
      pyhmy/exceptions.py
  6. 200
      pyhmy/signing.py
  7. 652
      pyhmy/staking.py
  8. 414
      pyhmy/staking_signing.py
  9. 218
      pyhmy/staking_structures.py
  10. 578
      pyhmy/transaction.py
  11. 50
      pyhmy/util.py
  12. 266
      pyhmy/validator.py
  13. 7
      setup.py
  14. 9
      tests/bech32-pyhmy/test_bech32.py
  15. 8
      tests/request-pyhmy/test_request.py
  16. 87
      tests/sdk-pyhmy/conftest.py
  17. 56
      tests/sdk-pyhmy/test_account.py
  18. 175
      tests/sdk-pyhmy/test_blockchain.py
  19. 74
      tests/sdk-pyhmy/test_contract.py
  20. 86
      tests/sdk-pyhmy/test_signing.py
  21. 104
      tests/sdk-pyhmy/test_staking.py
  22. 100
      tests/sdk-pyhmy/test_staking_signing.py
  23. 81
      tests/sdk-pyhmy/test_transaction.py
  24. 205
      tests/sdk-pyhmy/test_validator.py
  25. 20
      tests/util-pyhmy/test_util.py

@ -8,14 +8,12 @@ and [related codebases](https://github.com/harmony-one).
[Full documentation is located on Harmony's GitBook](https://docs.harmony.one/) (in progress).
## Installation
```
```bash
pip install pyhmy
```
On MacOS:
Make sure you have Python3 installed, and use python3 to install pyhmy
```bash
sudo pip3 install pathlib
sudo pip3 install pyhmy
```
@ -23,35 +21,544 @@ sudo pip3 install pyhmy
## Development
Clone the repository and then run the following:
```
```bash
make install
```
## Running tests
You need to run a local Harmony blockchain (instructions [here](https://github.com/harmony-one/harmony/blob/main/README.md)) that has staking enabled.
You can run all of the tests with the following:
Before you can run tests, you need the python dependencies (`make install`), `docker` and `go` installed to quickly run a local blockchain with staking enabled (detailed instructions [here](https://github.com/harmony-one/harmony/blob/main/README.md)):
```bash
mkdir -p $(go env GOPATH)/src/github.com/harmony-one
cd $(go env GOPATH)/src/github.com/harmony-one
git clone https://github.com/harmony-one/mcl.git
git clone https://github.com/harmony-one/bls.git
git clone https://github.com/harmony-one/harmony.git
cd harmony
make test-rpc
```
Once the terminal displays `=== FINISHED RPC TESTS ===`, use another shell to run the following tests
```bash
make test
```
Or directly with `pytest` (reference [here](https://docs.pytest.org/en/latest/index.html) for more info):
```
```bash
py.test tests
```
## Releasing
You can release this library with the following command (assuming you have the credentials to upload):
```bash
make release
```
## Usage
```py
test_net = 'https://api.s0.b.hmny.io' # this is shard 0
test_net_shard_1 = 'https://api.s1.b.hmny.io'
test_address = 'one18t4yj4fuutj83uwqckkvxp9gfa0568uc48ggj7'
main_net = 'https://rpc.s0.t.hmny.io'
main_net_shard_1 = 'https://rpc.s1.t.hmny.io'
```
make release
#### accounts
```py
from pyhmy import account
```
##### Balance / account related information
````py
balance = account.get_balance(test_address, endpoint=test_net) # on shard 0, in ATTO
total_balance = account.get_total_balance(test_address, endpoint=test_net) # on all shards, in ATTO
balance_by_shard = account.get_balance_on_all_shards(test_address, endpoint=test_net) # list of dictionaries with shard and balance as keys
genesis_balance = account.get_balance_by_block(test_address, block_num=0, endpoint=test_net)
latest_balance = account.get_balance_by_block(test_address, block_num='latest', endpoint=test_net) # block_num can be a string 'latest', or 'pending', if implemented at the RPC level
account_nonce = account.get_account_nonce(test_address, block_num='latest', endpoint=test_net)
````
##### Transaction counts
````py
tx_count = account.get_transactions_count(test_address, tx_type='ALL', endpoint=test_net)
sent_tx_count = account.get_transactions_count(test_address, tx_type='SENT', endpoint=test_net)
received_tx_count = account.get_transactions_count(test_address, tx_type='RECEIVED', endpoint=test_net)
legacy_tx_count = account.get_transaction_count(test_address, block_num='latest', endpoint=test_net) # API is legacy
legacy_tx_count_pending = account.get_transaction_count(test_address, block_num='pending', endpoint=test_net)
````
##### Staking transaction counts
````py
stx_count = account.get_staking_transactions_count(test_address, tx_type='ALL', endpoint=test_net)
sent_stx_count = account.get_staking_transactions_count(test_address, tx_type='SENT', endpoint=test_net)
received_stx_count = account.get_staking_transactions_count(test_address, tx_type='RECEIVED', endpoint=test_net)
````
##### Transaction history
To get a list of hashes, use `include_full_tx=False`
````py
first_100_tx_hashes = account.get_transaction_history(test_address, page=0, page_size=100, include_full_tx=False, endpoint=test_net)
````
To get the next 100 transactions, change the `page`
```py
next_100_tx_hashes = account.get_transaction_history(test_address, page=1, page_size=100, include_full_tx=False, endpoint=test_net)
```
To get a list of full transaction details, use `include_full_tx=True` (see `get_transaction_by_hash` for the reply structure
````py
first_3_full_tx = account.get_transaction_history(test_address, page=0, page_size=3, include_full_tx=True, endpoint=test_net)
````
To get newest transactions, use `order='DESC'`
````py
last_3_full_tx = account.get_transaction_history(test_address, page=0, page_size=3, include_full_tx=True, order='DESC', endpoint=test_net)
````
To change the transaction type (SENT / RECEIVED / ALL), pass the `tx_type` parameter
```py
first_100_received_tx_hashes = account.get_transaction_history(test_address, page=0, page_size=100, include_full_tx=False, tx_type='RECEIVED', endpoint=test_net)
```
##### Staking transaction history
To get a list of staking hashes, use `include_full_tx=False`
````py
first_100_stx_hashes = account.get_staking_transaction_history(test_address, page=0, page_size=100, include_full_tx=False, endpoint=test_net)
````
To get the next 100 staking transactions, change the `page`
```py
next_100_stx_hashes = account.get_staking_transaction_history(test_address, page=1, page_size=100, include_full_tx=False, endpoint=test_net)
```
To get a list of full staking transaction details, use `include_full_tx=True` (see `get_transaction_by_hash` for the reply structure
````py
first_3_full_stx = account.get_staking_transaction_history(test_address, page=0, page_size=3, include_full_tx=True, endpoint=test_net)
````
To get newest staking transactions, use `order='DESC'`
````py
last_3_full_stx = account.get_staking_transaction_history(test_address, page=0, page_size=3, include_full_tx=True, order='DESC', endpoint=test_net)
````
To change the staking transaction type (SENT / RECEIVED / ALL), pass the `tx_type` parameter
```py
first_100_received_stx_hashes = account.get_staking_transaction_history(test_address, page=0, page_size=100, include_full_tx=False, tx_type='RECEIVED', endpoint=test_net)
```
#### Blockchain
```py
from pyhmy import blockchain
from decimal import Decimal
```
##### Node / network information
```py
chain_id = blockchain.chain_id(test_net) # chain type, for example, mainnet or testnet
node_metadata = blockchain.get_node_metadata(test_net) # metadata about the endpoint
peer_info = blockchain.get_peer_info(test_net) # peers of the endpoint
protocol_version = blockchain.protocol_version(test_net) # protocol version being used
num_peers = blockchain.get_num_peers(test_net) # number of peers of the endpoin
version = blockchain.get_version(test_net) # EVM chain id, https://chainid.network
is_node_in_sync = blockchain.in_sync(test_net) # whether the node is in sync (not out of sync or not syncing)
is_beacon_in_sync = blockchain.beacon_in_sync(test_net) # whether the beacon node is in sync
prestaking_epoch_number = blockchain.get_prestaking_epoch(test_net)
staking_epoch_number = blockchain.get_staking_epoch(test_net)
```
##### Sharding information
```py
shard_id = blockchain.get_shard(test_net) # get shard id of the endpoint
sharding_structure = blockchain.get_sharding_structure(test_net) # list of dictionaries, each representing a shard
last_cross_links = blockchain.get_last_cross_links(test_net) # list of dictionaries for each shard except test_net
```
##### Current network status
```py
leader_address = blockchain.get_leader_address(test_net)
is_last_block = blockchain.is_last_block(block_num=0, test_net)
last_block_of_epoch5 = blockchain.epoch_last_block(block_num=5, test_net)
circulating_supply = Decimal(blockchain.get_circulating_supply(test_net))
premined = blockchain.get_total_supply(test_net) # should be None?
current_block_num = blockchain.get_block_number(test_net)
current_epoch = blockchain.get_current_epoch(test_net)
gas_price = blockchain.get_gas_price(test_net) # this returns 1 always
```
##### Block headers
```py
latest_header = blockchain.get_latest_header(test_net) # header contains hash, number, cross links, signature, time, etc (see get_latest_header for a full list)
latest_hash = latest_header['blockHash']
latest_number = latest_header['blockNumber']
previous_header = blockchain.get_header_by_number(latest_number-1, test_net)
chain_headers = blockchain.get_latest_chain_headers(test_net_shard_1) # chain headers by beacon and shard
```
##### Blocks
###### By block number
Fetch the barebones information about the block as a dictionary
```py
latest_block = blockchain.get_block_by_number(block_num='latest', endpoint=test_net)
```
Fetch a block with full information (`full_tx=True`) for each transaction in the block
```py
block = blockchain.get_block_by_number(block_num=9017724, full_tx=True, include_tx=True, include_staking_tx=True, endpoint=test_net)
```
Fetch a block and only staking transactions (`include_tx=False, include_staking_tx=True`) for the block
```py
block = blockchain.get_block_by_number(block_num='latest', include_tx=False, include_staking_tx=True, endpoint=test_net)
```
Fetch block signer addresses (`include_signers=True`) as a list
```py
signers = blockchain.get_block_by_number(block_num=9017724, include_signers=True, endpoint=test_net)['signers']
```
Or, alternatively, use the direct `get_block_signers` method:
```py
signers = blockchain.get_block_signers(block_num=9017724, endpoint=test_net)
```
Fetch the public keys for signers
```py
signers_keys = blockchain.get_block_signers_keys(block_num=9017724, endpoint=test_net)
```
Check if an address is a signer for a block
```py
is_block_signer = blockchain.is_block_signer(block_num=9017724, address='one1yc06ghr2p8xnl2380kpfayweguuhxdtupkhqzw', endpoint=test_net)
```
Fetch the number of blocks signed by a particular validator for the last epoch
```py
number_signed_blocks = blockchain.get_signed_blocks(address='one1yc06ghr2p8xnl2380kpfayweguuhxdtupkhqzw', endpoint=test_net)
```
Fetch a list of validators and their public keys for specific epoch number
```py
validators = blockchain.get_validators(epoch=12, endpoint=test_net)
validator_keys = blockchain.get_validator_keys(epoch=12, endpoint=test_net)
```
Fetch number of transactions
```py
tx_count = blockchain.get_block_transaction_count_by_number(block_num='latest', endpoint=test_net)
```
Fetch number of staking transactactions
```py
stx_count = blockchain.get_block_staking_transaction_count_by_number(block_num='latest', endpoint=test_net)
```
Fetch a list of blocks using the block numbers
```py
blocks = blockchain.get_blocks(start_block=0, end_block=2, full_tx=False, include_tx=False, include_staking_tx=False, include_signers=False, endpoint=test_net)
```
###### By block hash
Most of the functions described above can be applied for fetching information about a block whose hash is known, for example:
```py
block_hash = '0x44fa170c25f262697e5802098cd9eca72889a637ea52feb40c521f2681a6d720'
block = blockchain.get_block_by_hash(block_hash=block_hash, endpoint=test_net)
block_with_full_tx = blockchain.get_block_by_hash(block_hash=block_hash, full_tx=True, include_tx=True, include_staking_tx=True, endpoint=test_net)
block_with_only_staking_tx = blockchain.get_block_by_hash(block_hash=block_hash, include_tx=False, include_staking_tx=True, endpoint=test_net)
signers = blockchain.get_block_by_hash(block_hash=block_hash, include_signers=True, endpoint=test_net)['signers']
tx_count = blockchain.get_block_transaction_count_by_hash(block_hash=block_hash, endpoint=test_net)
stx_count = blockchain.get_block_staking_transaction_count_by_hash(block_hash=block_hash, endpoint=test_net)
```
#### Staking
```py
from pyhmy import staking
validator_addr = 'one1xjanr7lgulc0fqyc8dmfp6jfwuje2d94xfnzyd'
delegator_addr = 'one1y2624lg0mpkxkcttaj0c85pp8pfmh2tt5zhdte'
```
##### Validation
```py
all_validators = staking.get_all_validator_addresses(endpoint=test_net) # list of addresses
validator_information = staking.get_validator_information(validator_addr, endpoint=test_net) # dict with all info
validator_information_100 = staking.get_all_validator_information(page=0, endpoint=test_net) # for all use page=-1
elected_validators = staking.get_elected_validator_addresses(endpoint=test_net) # list of addresses
validators_for_epoch = staking.get_validators(epoch=73772, endpoint=test_net) # dict with list of validators and balance
validators_information_100_for_block = staking.get_all_validator_information_by_block_number(block_num=9017724, page=0, endpoint=test_net)
validator_keys_for_epoch = staking.get_validator_keys(epoch=73772, endpoint=test_net) # list of public keys
validator_information_at_block = staking.get_validator_information_by_block_number(validator_addr, block_num=9017724, endpoint=test_net)
self_delegation = staking.get_validator_self_delegation(validator_addr, endpoint=test_net)
total_delegation = staking.get_validator_total_delegation(validator_addr, endpoint=test_net)
```
##### Delegation
```py
delegation_information = staking.get_all_delegation_information(page=-1, endpoint=test_net)
delegations_by_delegator = staking.get_delegations_by_delegator(delegator_addr, test_net)
delegations_by_delegator_at_block = staking.get_delegations_by_delegator_by_block_number(delegator_addr, block_num=9017724, endpoint=test_net)
delegation_by_delegator_and_validator = staking.get_delegation_by_delegator_and_validator(delegator_addr, validator_addr, test_net)
avail_redelegation_balance = staking.get_available_redelegation_balance(delegator_addr, test_net)
delegations_by_validator = staking.get_delegations_by_validator(validator_addr, test_net) # list of delegations made to this validator, each a dictionary
```
##### Network
```py
utility_metrics = staking.get_current_utility_metrics(test_net)
network_info = staking.get_staking_network_info(test_net)
super_committees = staking.get_super_committees(test_net)
super_committees_current = super_committees['current'] # list of voting committees as a dict
super_committees_previous = super_committees['previous']
total_staking = staking.get_total_staking(endpoint=test_net) # by all validators, only for beaconchain
median_stake_snapshot = staking.get_raw_median_stake_snapshot(test_net)
```
##### Validator class
Instantiate a validator object and load it from the chain
```py
from pyhmy.validator import Validator
validator = Validator(validator_addr)
validator.load_from_blockchain(test_net)
```
Create a new validator object and load from dictionary
```py
from pyhmy.numbers import convert_one_to_atto
validator = Validator('one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9')
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)
}
validator.load(info)
```
Sign a validator creation transaction
```py
signed_create_tx_hash = validator.sign_create_validator_transaction(
nonce = 2,
gas_price = 1,
gas_limit = 100,
private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48',
chain_id = None).rawTransaction.hex()
```
To edit validator, change its parameters using the `setter` functions, for example, `validator.set_details`, except the `rate`, `bls_keys_to_add` and `bls_keys_to_remove` which can be passed to the below function:
```py
signed_edit_tx_hash = validator.sign_edit_validator_transaction(
nonce = 2,
gas_price = 1,
gas_limit = 100,
rate = '0.06',
bls_keys_to_add = "0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608611",
bls_keys_to_remove = '0xb9486167ab9087ab818dc4ce026edb5bf216863364c32e42df2af03c5ced1ad181e7d12f0e6dd5307a73b62247608612',
private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48',
chain_id = 2).rawTransaction.hex()
```
TODO: sample of how to use the library, reference Tezos.
TODO: start (and finish) some of the documentation.
TODO: add more blockchain rpcs
TODO: check None return types for rpcs
TODO: more detailed tests for rpcs
### Transactions
```py
from pyhmy import transaction
```
##### Pool
```py
pending_tx = transaction.get_pending_transactions(test_net)
pending_stx = transaction.get_pending_staking_transactions(test_net)
tx_error_sink = transaction.get_transaction_error_sink(test_net)
stx_error_sink = transaction.get_staking_transaction_error_sink(test_net)
pool_stats = transaction.get_pool_stats(test_net)
pending_cx_receipts = transaction.get_pending_cx_receipts(test_net)
```
##### Fetching transactions
```py
tx_hash = '0x500f7f0ee70f866ba7e80592c06b409fabd7ace018a9b755a7f1f29e725e4423'
block_hash = '0xb94bf6e8a8a970d4d42dfe42f7f231af0ff7fd54e7f410395e3b306f2d4000d4'
tx = transaction.get_transaction_by_hash(tx_hash, test_net) # dict with tx-level info like from / to / gas
tx_from_block_hash = transaction.get_transaction_by_block_hash_and_index(block_hash, tx_index=0, endpoint=test_net)
tx_from_block_number = transaction.get_transaction_by_block_number_and_index(9017724, tx_index=0, endpoint=test_net)
tx_receipt = transaction.get_transaction_receipt(tx_hash, test_net)
```
##### Fetching staking transactions
```py
stx_hash = '0x3f616a8ef34f111f11813630cdcccb8fb6643b2affbfa91d3d8dbd1607e9bc33'
block_hash = '0x294dc88c7b6f3125f229a3cfd8d9b788a0bcfe9409ef431836adcd83839ba9f0' # block number 9018043
stx = transaction.get_staking_transaction_by_hash(stx_hash, test_net)
stx_from_block_hash = transaction.get_staking_transaction_by_block_hash_and_index(block_hash, tx_index=0, endpoint=test_net)
stx_from_block_number = transaction.get_staking_transaction_by_block_number_and_index(9018043, tx_index=0, endpoint=test_net)
```
##### Cross shard transactions
```py
cx_hash = '0xd324cc57280411dfac5a7ec2987d0b83e25e27a3d5bb5d3531262387331d692b'
cx_receipt = transaction.get_cx_receipt_by_hash(cx_hash, main_net_shard_1) # the shard which receives the tx
tx_resent = transaction.resend_cx_receipt(cx_hash, main_net) # beacon chain
```
##### Sending transactions
Sign it with your private key and use `send_raw_transaction`
```py
from pyhmy import signing
tx = {
'chainId': 2,
'from': 'one18t4yj4fuutj83uwqckkvxp9gfa0568uc48ggj7',
'gas': 6721900,
'gasPrice': 1000000000,
'nonce': 6055,
'shardID': 0,
'to': 'one1ngt7wj57ruz7kg4ejp7nw8z7z6640288ryckh9',
'toShardID': 0,
'value': 500000000000000000000
}
transaction.send_raw_transaction(signing.sign_transaction(tx, '01F903CE0C960FF3A9E68E80FF5FFC344358D80CE1C221C3F9711AF07F83A3BD').rawTransaction.hex(), test_net)
```
A similar approach can be followed for staking transactions
```py
from pyhmy import staking_structures, staking_signinge
tx = {
'chainId': 2,
'delegatorAddress': 'one18t4yj4fuutj83uwqckkvxp9gfa0568uc48ggj7',
'directive': staking_structures.Directive.CollectRewards,
'gasLimit': 6721900,
'gasPrice': 1,
'nonce': 6056
}
transaction.send_raw_staking_transaction(staking_signing.sign_staking_transaction(tx, private_key = '01F903CE0C960FF3A9E68E80FF5FFC344358D80CE1C221C3F9711AF07F83A3BD').rawTransaction.hex(), test_net)
```
### Contracts
```py
from pyhmy import contract
from pyhmy.util import convert_one_to_hex
contract_addr = 'one1rcs4yy4kln53ux60qdeuhhvpygn2sutn500dhw'
```
Call a contract without saving state
```py
from pyhmy import numbers
result = contract.call(convert_one_to_hex(contract_addr), 'latest', value=hex(int(numbers.convert_one_to_atto(5)))
, gas_price=hex(1), gas=hex(100000), endpoint=test_net)
```
Estimate gas required for a smart contract call
```py
estimated_gas = contract.estimate_gas(convert_one_to_hex(contract_addr), endpoint=test_net)
```
Fetch the byte code of the contract
```py
byte_code = contract.get_code(convert_one_to_hex(contract_addr), 'latest', endpoint=test_net)
```
Get storage in the contract at `key`
```py
storage = contract.get_storage_at(convert_one_to_hex(contract_addr), key='0x0', block_num='latest', endpoint=test_net)
```
Calling a function on a contract needs the contract ABI. The ABI can be obtained by compiling the contract.
```py
from web3 import Web3
from web3 import providers
from pyhmy.util import convert_one_to_hex
contract_abi = '[{"constant":true,"inputs":[],"name":"manager","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"pickWinner","outputs":[],"payable":false,"stateMutability":"nonpayable","type":"function"},{"constant":true,"inputs":[],"name":"getPlayers","outputs":[{"name":"","type":"address[]"}],"payable":false,"stateMutability":"view","type":"function"},{"constant":false,"inputs":[],"name":"enter","outputs":[],"payable":true,"stateMutability":"payable","type":"function"},{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"players","outputs":[{"name":"","type":"address"}],"payable":false,"stateMutability":"view","type":"function"},{"inputs":[],"payable":false,"stateMutability":"nonpayable","type":"constructor"}]'
w3 = Web3(providers.HTTPProvider(test_net))
lottery = w3.eth.contract(abi=contract_abi, address=convert_one_to_hex('one1rcs4yy4kln53ux60qdeuhhvpygn2sutn500dhw'))
lottery.functions.getPlayers().call()
```
To actually participate in a contract, you can sign a transaction from your account to it.
```py
from pyhmy import signing
contract_addr = 'one1rcs4yy4kln53ux60qdeuhhvpygn2sutn500dhw'
tx = {
'chainId': 2,
'from': 'one18t4yj4fuutj83uwqckkvxp9gfa0568uc48ggj7',
'gas': 6721900,
'gasPrice': 1000000000,
'nonce': 6054,
'shardID': 0,
'to': contract_addr,
'toShardID': 0,
'value': 500000000000000000000
}
tx_hash = transaction.send_raw_transaction(signing.sign_transaction(tx, '01F903CE0C960FF3A9E68E80FF5FFC344358D80CE1C221C3F9711AF07F83A3BD').rawTransaction.hex(), test_net)
```
To deploy a contract, sign a transaction from your account without a `to` field and with the byte code as `data` and send it.
```py
from pyhmy import signing
from pyhmy import transaction
contract_tx = {
'chainId': 2, # test net data
'data': '0x608060405234801561001057600080fd5b50600436106100415760003560e01c8063445df0ac146100465780638da5cb5b14610064578063fdacd576146100ae575b600080fd5b61004e6100dc565b6040518082815260200191505060405180910390f35b61006c6100e2565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100da600480360360208110156100c457600080fd5b8101908080359060200190929190505050610107565b005b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561016457806001819055505b5056fea265627a7a723158209b80813a158b44af65aee232b44c0ac06472c48f4abbe298852a39f0ff34a9f264736f6c63430005100032', # Migrations.sol
'from': 'one18t4yj4fuutj83uwqckkvxp9gfa0568uc48ggj7',
'gas': 6721900,
'gasPrice': 1000000000,
'nonce': 6049,
'shardID': 0,
'toShardID': 0
}
ctx_hash = transaction.send_raw_transaction(signing.sign_transaction(contract_tx, private_key = '01F903CE0C960FF3A9E68E80FF5FFC344358D80CE1C221C3F9711AF07F83A3BD').rawTransaction.hex(), test_net)
# the below may be need a time gap before the transaction reaches the chain
contract_address = transaction.get_transaction_receipt(ctx_hash, test_net)['contractAddress']
```
### Signing transactions
```py
from pyhmy import signing
```
Create a `transaction_dict` with the parameters, and supply your private key to sign (but not submit) a transaction. A signed transaction can be submitted using `transaction.sendRawTransaction`.
```py
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, private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48')
signed_hash = signed_tx.rawTransaction.hex()
```
For a transaction with is Ethereum-like, the `shardID` and `toShardID` are optional, which implies that the transaction is not cross-shard.
```py
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, private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48')
signed_hash = signed_tx.rawTransaction.hex()
```
The `chainId` parameter is also optional, and [according to Ethereum](https://github.com/ethereum/eth-account/blob/00e7b10005c5fa7090086fcef37a76296c524e17/eth_account/_utils/transactions.py#L122), it should not be passed if "you want a transaction that can be replayed across networks." A full list of the possible values of `chainId` is provided below. You can pass either the `str` or the `int`. The RPC API may, however, reject the transaction, which is why it is recommended to pass either `1` or `2` for `mainnet` and `testnet` respectively.
```py
Default = 0,
EthMainnet = 1,
Morden = 2,
Ropsten = 3,
Rinkeby = 4,
RootstockMainnet = 30,
RootstockTestnet = 31,
Kovan = 42,
EtcMainnet = 61,
EtcTestnet = 62,
Geth = 1337,
Ganache = 0,
HmyMainnet = 1,
HmyTestnet = 2,
HmyLocal = 2,
HmyPangaea = 3,
```
### Signing staking transactions
```py
from pyhmy import staking_structures, staking_signing
```
To sign a transaction to collect rewards, supply the dictionary containing the `delegatorAddress` and the private key.
```py
transaction_dict = {
'directive': staking_structures.Directive.CollectRewards,
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9',
'nonce': 2,
'gasPrice': 1,
'gasLimit': 100,
}
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, private_key = '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48')
```
To sign a transaction to delegate or undelegate, supply the dictionary containing the `delegatorAddress`, the `validatorAddress`, the `amount` to delegate or undelegate, and the private key.
```py
transaction_dict = {
'directive': staking_structures.Directive.Delegate,
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9',
'validatorAddress': 'one1xjanr7lgulc0fqyc8dmfp6jfwuje2d94xfnzyd',
'amount': 5,
'nonce': 2,
'gasPrice': 1,
'gasLimit': 100,
}
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48')
transaction_dict = {
'directive': staking_structures.Directive.Undelegate,
'delegatorAddress': 'one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9',
'validatorAddress': 'one1xjanr7lgulc0fqyc8dmfp6jfwuje2d94xfnzyd',
'amount': 5,
'nonce': 2,
'gasPrice': 1,
'gasLimit': 100,
}
signed_tx = staking_signing.sign_staking_transaction(transaction_dict, '4edef2c24995d15b0e25cbd152fb0e2c05d3b79b9c2afd134e6f59f91bf99e48')
```
For validator-related transactions, see the [section on the Validator class](#validator-class).
## Keeping your private key safe
You need `eth-keyfile` installed
```bash
pip install eth-keyfile
```
In a `Python` shell, you can save or load the key into / from a key file.
```py
import eth_keyfile
from eth_utils import to_bytes, to_hex
import json
keyfile = eth_keyfile.create_keyfile_json(to_bytes(hexstr='01F903CE0C960FF3A9E68E80FF5FFC344358D80CE1C221C3F9711AF07F83A3BD'), b'password')
with open('keyfile.json', 'w+') as outfile:
json.dump(keyfile, outfile)
private_key = to_hex(eth_keyfile.extract_key_from_keyfile('keyfile.json', b'password'))[2:].upper()
```

@ -16,7 +16,7 @@ from .blockchain import (
get_sharding_structure
)
from bech32 import (
from .bech32.bech32 import (
bech32_decode
)
@ -69,19 +69,21 @@ def get_balance(address, endpoint=_default_endpoint, timeout=_default_timeout) -
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#da8901d2-d237-4c3b-9d7d-10af9def05c4
"""
method = 'hmy_getBalance'
method = 'hmyv2_getBalance'
params = [
address,
'latest'
address
]
balance = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return int(balance, 16)
except TypeError as e:
balance = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
return int(balance) # v2 returns the result as it is
except TypeError as e: # check will work if rpc returns None
raise InvalidRPCReplyError(method, endpoint) from e
def get_balance_by_block(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get account balance for address at a given block number
@ -106,20 +108,24 @@ def get_balance_by_block(address, block_num, endpoint=_default_endpoint, timeout
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#9aeae4b8-1a09-4ed2-956b-d7c96266dd33
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/blockchain.go#L92
"""
method = 'hmy_getBalanceByBlockNumber'
method = 'hmyv2_getBalanceByBlockNumber'
params = [
address,
str(hex(block_num))
block_num
]
balance = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return int(balance, 16)
balance = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
return int(balance)
except TypeError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_account_nonce(address, true_nonce=False, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
def get_account_nonce(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get the account nonce
@ -127,9 +133,8 @@ def get_account_nonce(address, true_nonce=False, endpoint=_default_endpoint, tim
----------
address: str
Address to get transaction count for
true_nonce: :obj:`bool`, optional
True to get on-chain nonce
False to get nonce based on pending transaction pool
block_num: :obj:`int` or 'latest'
Block to get nonce at
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -144,27 +149,75 @@ def get_account_nonce(address, true_nonce=False, endpoint=_default_endpoint, tim
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L51
"""
method = 'hmy_getTransactionCount'
method = 'hmyv2_getAccountNonce'
params = [
address,
'latest' if true_nonce else 'pending'
block_num
]
nonce = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return int(nonce, 16)
nonce = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
return int(nonce)
except TypeError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_transaction_count(address, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get the number of transactions the given address has sent for the given block number
Legacy for apiv1. For apiv2, please use get_account_nonce/get_transactions_count/get_staking_transactions_count apis for
more granular transaction counts queries
Parameters
----------
address: str
Address to get transaction count for
block_num: :obj:`int` or 'latest'
Block to get nonce at
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
int
The number of transactions the given address has sent for the given block number
def get_transaction_count(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L69
"""
method = 'hmyv2_getTransactionCount'
params = [
address,
block_num
]
try:
nonce = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
return int(nonce)
except TypeError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_transactions_count(address, tx_type, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get number of transactions & staking transactions sent by an account
Get the number of regular transactions from genesis of input type
Parameters
----------
address: str
Address to get transaction count for
tx_type: str
Type of transactions to include in the count
currently supported are 'SENT', 'RECEIVED', 'ALL'
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -173,14 +226,70 @@ def get_transaction_count(address, endpoint=_default_endpoint, timeout=_default_
Returns
-------
int
Number of transactions sent by the account
Count of transactions of type tx_type
See also
--------
get_account_nonce
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#fc97aed2-e65e-4cf4-bc01-8dadb76732c0
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L114
"""
return get_account_nonce(address, true_nonce=True, endpoint=endpoint, timeout=timeout)
method = 'hmyv2_getTransactionsCount'
params = [
address,
tx_type
]
try:
tx_count = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
return int(tx_count)
except TypeError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_staking_transactions_count(address, tx_type, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get the number of staking transactions from genesis of input type ("SENT", "RECEIVED", "ALL")
Parameters
----------
address: str
Address to get staking transaction count for
tx_type: str
Type of staking transactions to include in the count
currently supported are 'SENT', 'RECEIVED', 'ALL'
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
int
Count of staking transactions of type tx_type
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#ddc1b029-f341-4c4d-ba19-74b528d6e5e5
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L134
"""
method = 'hmyv2_getStakingTransactionsCount'
params = [
address,
tx_type
]
try:
tx_count = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
return int(tx_count)
except (KeyError, TypeError) as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_transaction_history(address, page=0, page_size=1000, include_full_tx=False, tx_type='ALL',
order='ASC', endpoint=_default_endpoint, timeout=_default_timeout
@ -204,8 +313,8 @@ def get_transaction_history(address, page=0, page_size=1000, include_full_tx=Fal
'SENT' to get all transactions sent by the address
'RECEIVED' to get all transactions received by the address
order: :obj:`str`, optional
'ASC' to sort transactions in ascending order based on timestamp
'DESC' to sort transactions in descending order based on timestamp
'ASC' to sort transactions in ascending order based on timestamp (oldest first)
'DESC' to sort transactions in descending order based on timestamp (newest first)
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -213,13 +322,20 @@ def get_transaction_history(address, page=0, page_size=1000, include_full_tx=Fal
Returns
-------
list
# TODO: Add link to reference RPC documentation
list of transactions
if include_full_tx is True, each transaction is a dictionary with the following keys
see transaction/get_transaction_by_hash for a description
if include_full_tx is False, each element represents the transaction hash
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#2200a088-81b5-4420-a291-312a7c6d880e
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L255
"""
params = [
{
@ -231,14 +347,13 @@ def get_transaction_history(address, page=0, page_size=1000, include_full_tx=Fal
'order': order
}
]
method = 'hmy_getTransactionsHistory'
tx_history = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)
method = 'hmyv2_getTransactionsHistory'
try:
tx_history = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)
return tx_history['result']['transactions']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_staking_transaction_history(address, page=0, page_size=1000, include_full_tx=False, tx_type='ALL',
order='ASC', endpoint=_default_endpoint, timeout=_default_timeout
) -> list:
@ -268,13 +383,33 @@ def get_staking_transaction_history(address, page=0, page_size=1000, include_ful
Returns
-------
list
# TODO: Add link to reference RPC documentation
list of transactions
if include_full_tx is True, each transaction is a dictionary with the following kets
blockHash: :obj:`str` Block hash that transaction was finalized; "0x0000000000000000000000000000000000000000000000000000000000000000" if tx is pending
blockNumber: :obj:`int` Block number that transaction was finalized; None if tx is pending
from: :obj:`str` Wallet address
timestamp: :obj:`int` Timestamp in Unix time when transaction was finalized
gas: :obj:`int` Gas limit in Atto
gasPrice :obj:`int` Gas price in Atto
hash: :obj:`str` Transaction hash
nonce: :obj:`int` Wallet nonce for the transaction
transactionIndex: :obj:`int` Index of transaction in block; None if tx is pending
type: :obj:`str` Type of staking transaction, for example, "CollectRewards", "Delegate", "Undelegate"
msg: :obj:`dict` Message attached to the staking transaction
r: :obj:`str` First 32 bytes of the transaction signature
s: :obj:`str` Next 32 bytes of the transaction signature
v: :obj:`str` Recovery value + 27, as hex string
if include_full_tx is False, each element represents the transaction hash
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#c5d25b36-57be-4e43-a23b-17ace350e322
https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/rpc/transaction.go#L303
"""
params = [
{
@ -288,13 +423,12 @@ def get_staking_transaction_history(address, page=0, page_size=1000, include_ful
]
# Using v2 API, because getStakingTransactionHistory not implemented in v1
method = 'hmyv2_getStakingTransactionsHistory'
stx_history = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
try:
stx_history = rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
return stx_history['staking_transactions']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_balance_on_all_shards(address, skip_error=True, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get current account balance in all shards & optionally report errors getting account balance for a shard
@ -313,8 +447,7 @@ def get_balance_on_all_shards(address, skip_error=True, endpoint=_default_endpoi
Returns
-------
list
Account balance per shard in ATTO
list of dictionaries, each dictionary to contain shard number and balance of that shard in ATTO
Example reply:
[
{
@ -340,7 +473,6 @@ def get_balance_on_all_shards(address, skip_error=True, endpoint=_default_endpoi
})
return balances
def get_total_balance(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get total account balance on all shards
@ -363,6 +495,10 @@ def get_total_balance(address, endpoint=_default_endpoint, timeout=_default_time
------
RuntimeError
If error occurred getting account balance for a shard
See also
------
get_balance_on_all_shards
"""
try:
balances = get_balance_on_all_shards(address, skip_error=False, endpoint=endpoint, timeout=timeout)

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

@ -33,3 +33,12 @@ class InvalidValidatorError(ValueError):
def __str__(self):
return f'[Errno {self.code}] {self.errors[self.code]}: {self.msg}'
class TxConfirmationTimedoutError(AssertionError):
"""
Exception raised when a transaction is sent to the chain
But not confirmed during the timeout period specified
"""
def __init__(self, msg):
super().__init__(f'{msg}')

@ -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,
)

@ -5,7 +5,6 @@ from .rpc.request import (
_default_endpoint = 'http://localhost:9500'
_default_timeout = 30
##################
# Validator RPCs #
##################
@ -23,10 +22,22 @@ def get_all_validator_addresses(endpoint=_default_endpoint, timeout=_default_tim
Returns
-------
list
List of one addresses for all validators on chain
"""
return rpc_request('hmy_getAllValidatorAddresses', endpoint=endpoint, timeout=timeout)['result']
List of :obj:`str`, one addresses for all validators on chain
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#69b93657-8d3c-4d20-9c9f-e51f08c9b3f5
"""
method = 'hmyv2_getAllValidatorAddresses'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_validator_information(validator_addr, endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
"""
@ -43,16 +54,176 @@ def get_validator_information(validator_addr, endpoint=_default_endpoint, timeou
Returns
-------
dict
# TODO: Add link to reference RPC documentation
:obj:`dict` Dictionary with the following keys
validator: :obj:`dict` Dictionary with the following keys
address: :obj:`str` Address of the validator
bls-public-keys: :obj:`list` List of associated public BLS keys
last-epoch-in-committee: :obj:`int` Last epoch any key of the validator was elected
min-self-delegation: :obj:`int` Amount that validator must delegate to self in ATTO
max-total-delegation: :obj:`int` Total amount that validator will aceept delegations until, in ATTO
rate: :obj:`str` Current commission rate
max-rate: :obj:`str` Max commission rate a validator can charge
max-change-rate: :obj:`str` Maximum amount the commission rate can increase in one epoch
update-height: :obj:`int` Last block number as which validator edited their information
name: :obj:`str` Validator name, displayed on the Staking Dashboard
identity: :obj:`str` Validator identity, must be unique
website: :obj:`str` Validator website, displayed on the Staking Dashboard
security-contact: :obj:`str` Method to contact the validators
details: :obj:`str` Validator details, displayed on the Staking Dashboard
creation-height: :obj:`int` Block number in which the validator was created
delegations: :obj:`list` List of delegations, see get_delegations_by_delegator for format
metrics: :obj:`dict` BLS key earning metrics for current epoch (or None if no earnings in the current epoch)
by-bls-key: :obj:`list` List of dictionaries, each with the following keys
key: :obj:`dict` Dictionary with the following keys
bls-public-key: :obj:`str` BLS public key
group-percent: :obj:`str` Key voting power in shard
effective-stake: :obj:`str` Effective stake of key
raw-stake: :obj:`str` Actual stake of key
earning-account: :obj:`str` Validator wallet address
overall-percent: :obj:`str` Percent of effective stake
shard-id: :obj:`int` Shard ID that key is on
earned-reward: :obj:`int` Lifetime reward key has earned
total-delegation: :obj:`int` Total amount delegated to validator
currently-in-committee: :obj:`bool` if key is currently elected
epos-status: :obj:`str` Currently elected, eligible to be elected next epoch, or not eligible to be elected next epoch
epos-winning-stake: :obj:`str` Total effective stake of the validator
booted-status: :obj:`str` Banned status
active-status: :obj:`str` Active or inactive
lifetime: :obj:`dict` Lifetime statistics as following keys:
reward-accumulated: :obj:`int` Lifetime reward accumulated by the validator
blocks: :obj:`dict` with the following keys
to-sign: :obj:`int` Number of blocks available in the validator to sign
signed: :obj:`int` Number of blocks the validator has signed
apr: :obj:`str` Approximate return rate
epoch-apr: :obj:`list` List of APR per epoch
epoch: :obj:`int` Epoch number
value: :obj:`str` Calculated APR for that epoch
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#659ad999-14ca-4498-8f74-08ed347cab49
"""
method = 'hmyv2_getValidatorInformation'
params = [
validator_addr
]
return rpc_request('hmy_getValidatorInformation', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_elected_validator_addresses(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get list of elected validator addresses
Parameters
----------
validator_addr: str
One address of the validator to get information for
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
:obj:`list` List of wallet addresses that are currently elected
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#e90a6131-d67c-4110-96ef-b283d452632d
"""
method = 'hmyv2_getElectedValidatorAddresses'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_validators(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get validators list for a particular epoch
Parameters
----------
epoch: :obj:`int`
epoch number
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
:obj:`dict` Dictionary with the following keys:
shardID: :obj:`int` Shard ID of the endpoint
validators: obj:`list` List of dictionaries
address: obj:`str` One address of validator
balance: :obj:`int` Balance of validator
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L152
"""
method = 'hmyv2_getValidators'
params = [
epoch
]
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_validator_keys(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get validator BLS keys in the committee for a particular epoch
Parameters
----------
epoch: :obj:`int`
epoch number
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
:obj:`list` List of public keys in the elected committee
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L152
"""
method = 'hmyv2_getValidatorKeys'
params = [
epoch
]
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_validator_information_by_block(validator_addr, block_num, endpoint=_default_endpoint, timeout=_default_timeout):
def get_validator_information_by_block_number(validator_addr, block_num, endpoint=_default_endpoint, timeout=_default_timeout):
"""
Get validator information for validator address at a block
@ -69,15 +240,26 @@ def get_validator_information_by_block(validator_addr, block_num, endpoint=_defa
Returns
-------
list
# TODO: Add link to reference RPC documentation
dict, see get_validator_information for structure
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L319
"""
method = 'hmyv2_getValidatorInformationByBlockNumber'
params = [
validator_addr,
str(hex(block_num))
block_num
]
return rpc_request('hmy_getValidatorInformationByBlockNumber', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_all_validator_information(page=-1, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
@ -86,7 +268,7 @@ def get_all_validator_information(page=-1, endpoint=_default_endpoint, timeout=_
Parameters
----------
page: :obj:`int`, optional
Page to request (-1 for all validators)
Page to request (-1 for all validators), page size is 100 otherwise
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -94,16 +276,97 @@ def get_all_validator_information(page=-1, endpoint=_default_endpoint, timeout=_
Returns
-------
list
# TODO: Add link to reference RPC documentation
list of validators, see get_validator_information for description
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#df5f1631-7397-48e8-87b4-8dd873235b9c
"""
method = 'hmyv2_getAllValidatorInformation'
params = [
page
]
return rpc_request('hmy_getAllValidatorInformation', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_validator_self_delegation(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get the amount self delegated by validator
def get_all_validator_information_by_block(block_num, page=-1, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
Parameters
----------
address: :obj:`str`
one address of the validator
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
int, validator stake in ATTO
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L352
"""
method = 'hmyv2_getValidatorSelfDelegation'
params = [
address
]
try:
return int(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'])
except (KeyError, TypeError) as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_validator_total_delegation(address, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get the total amount delegated t ovalidator (including self delegated)
Parameters
----------
address: :obj:`str`
one address of the validator
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
int, total validator stake in ATTO
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L379
"""
method = 'hmyv2_getValidatorTotalDelegation'
params = [
address
]
try:
return int(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'])
except (KeyError, TypeError) as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_all_validator_information_by_block_number(block_num, page=-1, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get validator information at block number for all validators on chain
@ -112,7 +375,7 @@ def get_all_validator_information_by_block(block_num, page=-1, endpoint=_default
block_num: int
Block number to get validator information for
page: :obj:`int`, optional
Page to request (-1 for all validators)
Page to request (-1 for all validators), page size is 100
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -120,27 +383,76 @@ def get_all_validator_information_by_block(block_num, page=-1, endpoint=_default
Returns
-------
list
# TODO: Add link to reference RPC documentation
list of validators, see get_validator_information for description
note that metrics field is overwritten & will always display current epoch data
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#a229253f-ca76-4b9d-88f5-9fd96e40d583
"""
method = 'hmyv2_getAllValidatorInformationByBlockNumber'
params = [
page,
str(hex(block_num))
block_num
]
return rpc_request('hmy_getAllValidatorInformationByBlockNumber', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
###################
# Delegation RPCs #
###################
def get_all_delegation_information(page=-1, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get delegation information for all delegators on chain
Parameters
----------
page: :obj:`int`, optional
Page to request (-1 for all validators), page size is 100
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
list of list of dictionaries
each sub-list will have the same validator but different delegator
each dictionary represents a dict, see get_delegations_by_delegator for description
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L413
"""
method = 'hmyv2_getAllDelegationInformation'
params = [
page,
]
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_delegations_by_delegator(delegator_addr, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get list of delegations by a delegator
Parameters
----------
delegator_addr: str
Delegator address to get list of delegations for
delegator_addr: :obj:`str`
Address of the delegator
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -148,16 +460,34 @@ def get_delegations_by_delegator(delegator_addr, endpoint=_default_endpoint, tim
Returns
-------
list
# TODO: Add link to reference RPC documentation
:obj:`list` List of delegations, each a dict with the following keys
validator_address: :obj:`str` Validator wallet address
delegator_address: :obj:`str` Delegator wallet address
amount: :obj:`int` Amount delegated in ATTO
reward: :obj:`int` Unclaimed rewards in ATTO
Undelegations: :obj:`dict` List of pending undelegations, each a dict
Amount: :obj:`int` Amount to be undelegated in ATTO
Epoch: :obj:`int` Epoch number of the undelegation
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#454b032c-6072-4ecb-bf24-38b3d6d2af69
"""
method = 'hmyv2_getDelegationsByDelegator'
params = [
delegator_addr
]
return rpc_request('hmy_getDelegationsByDelegator', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_delegations_by_delegator_by_block(delegator_addr, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
def get_delegations_by_delegator_by_block_number(delegator_addr, block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get list of delegations by a delegator at a specific block
@ -174,15 +504,100 @@ def get_delegations_by_delegator_by_block(delegator_addr, block_num, endpoint=_d
Returns
-------
list
# TODO: Add link to reference RPC documentation
list of delegations, see get_delegations_by_delegator for fields
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#8ce13bda-e768-47b9-9dbe-193aba410b0a
"""
method = 'hmyv2_getDelegationsByDelegatorByBlockNumber'
params = [
delegator_addr,
str(hex(block_num))
block_num
]
return rpc_request('hmy_getDelegationsByDelegatorByBlockNumber', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_delegation_by_delegator_and_validator(delegator_addr, validator_address,
endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
"""
Get list of delegations by a delegator at a specific block
Parameters
----------
delegator_addr: str
Delegator address to get delegation for
validator_addr: str
Validator address to get delegation for
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
one delegation (or None if such delegation doesn't exist), see get_delegations_by_delegator for fields
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L605
"""
method = 'hmyv2_getDelegationByDelegatorAndValidator'
params = [
delegator_addr,
validator_address
]
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_available_redelegation_balance(delegator_addr, endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get amount of locked undelegated tokens
Parameters
----------
delegator_addr: str
Delegator address to amount of locked undelegated tokens
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
int, representing the amount of locked undelegated tokens in ATTO
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L653
"""
method = 'hmyv2_getAvailableRedelegationBalance'
params = [
delegator_addr
]
try:
return int(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'])
except (KeyError, TypeError) as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_delegations_by_validator(validator_addr, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
@ -199,14 +614,25 @@ def get_delegations_by_validator(validator_addr, endpoint=_default_endpoint, tim
Returns
-------
list
# TODO: Add link to reference RPC documentation
list of delegations, see get_delegations_by_delegator for fields
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#2e02d8db-8fec-41d9-a672-2c9862f63f39
"""
method = 'hmyv2_getDelegationsByValidator'
params = [
validator_addr
]
return rpc_request('hmy_getDelegationsByValidator', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
########################
# Staking Network RPCs #
@ -224,11 +650,26 @@ def get_current_utility_metrics(endpoint=_default_endpoint, timeout=_default_tim
Returns
-------
dict
# TODO: Add link to reference RPC documentation
:obj: `dict` with the following keys:
AccumulatorSnapshot: :obj:`int` Total block reward given out in ATTO
CurrentStakedPercentage: :obj:`str` Percent of circulating supply staked
Deviation: :obj:`str` Change in percentage of circulating supply staked
Adjustment: :obj:`str` Change in circulating supply staked
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#78dd2d94-9ff1-4e0c-bbac-b4eec1cdf10b
"""
return rpc_request('hmy_getCurrentUtilityMetrics', endpoint=endpoint, timeout=timeout)['result']
method = 'hmyv2_getCurrentUtilityMetrics'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_staking_network_info(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
"""
@ -243,11 +684,27 @@ def get_staking_network_info(endpoint=_default_endpoint, timeout=_default_timeou
Returns
-------
dict
# TODO: Add link to reference RPC documentation
:obj: `dict` with the following keys
total-supply: :obj:`str` Total number of pre-mined tokens
circulating-supply: :obj:`str` Number of tokens available in the network
epoch-last-block: :obj:`int` Last block of epoch
total-staking: :obj:`int` Total amount staked in ATTO
median-raw-stake: :obj:`int` Effective median stake in ATTO
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#4a10fce0-2aa4-4583-bdcb-81ee0800993b
"""
return rpc_request('hmy_getStakingNetworkInfo', endpoint=endpoint, timeout=timeout)['result']
method = 'hmyv2_getStakingNetworkInfo'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_super_committees(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
"""
@ -262,11 +719,75 @@ def get_super_committees(endpoint=_default_endpoint, timeout=_default_timeout) -
Returns
-------
dict
# TODO: Add link to reference RPC documentation
dict with two keys, 'previous' and 'current', each a dict with the following keys
quorum-deciders: :obj:`dict` dictionary with keys
shard-X: :obj:`dict` Shard of committees, with the following keys
committee-members: :obj:`list` List of committee members
bls-public-key: :obj:`str` BLS public key
earning-account: :obj:`str` Wallet address to which rewards are being paid
is-harmony-slot: :obj:`bool` if slot is Harmony owned
voting-power-%: :obj:`str` Normalized voting power of key
voting-power-unnormalized: :obj:`str` Voting power of key
policy: :obj:`str` Current election policy
count: :obj:`int` Number of BLS keys on shard
external-validator-slot-count: :obj:`int` Number of external BLS keys in committee
hmy-voting-power: :obj:`str` Voting power of harmony in percent
staked-voting-power: :obj:`str` Voting power that is staked
total-effective-stake: :obj:`str` Total effective stake
total-raw-stake: :obj:`str` Total raw stake
is-harmony-slot - Boolean : If slot is Harmony owned
earning-account - String : Wallet address that rewards are being paid to
bls-public-key - String : BLS public key
voting-power-unnormalized - String : Voting power of key
voting-power-% - String
epoch: :obj:`int` Current / previous epoch
epos-median-stake: :obj:`str` Effective median stake
external-slot-count: :obj:`int` Available committee slots
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#8eef2fc4-92db-4610-a9cd-f7b75cfbd080
"""
return rpc_request('hmy_getSuperCommittees', endpoint=endpoint, timeout=timeout)['result']
method = 'hmyv2_getSuperCommittees'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_total_staking(endpoint=_default_endpoint, timeout=_default_timeout) -> int:
"""
Get total staking by validators, only meant to be called on beaconchain
Parameters
----------
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
int with total staking by validators
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/staking.go#L102
"""
method = 'hmyv2_getTotalStaking'
try:
return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'])
except (KeyError, TypeError) as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_raw_median_stake_snapshot(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
"""
@ -281,7 +802,32 @@ def get_raw_median_stake_snapshot(endpoint=_default_endpoint, timeout=_default_t
Returns
-------
dict
# TODO: Add link to reference RPC documentation
:obj: `dict` Dictionary with the following keys
epos-median-stake: :obj:`str` Effective median stake
max-external-slots: :obj:`int` Number of available committee slots
epos-slot-winners: :obj:`list` List of dictionaries, each with the following keys
slot-owner: :obj:`str` Wallet address of BLS key
bls-public-key: :obj:`str` BLS public key
raw-stake: :obj:`str` Actual stake
eposed-stake: :obj:`str` Effective stake
epos-slot-candidates: :obj:`list` List of dictionaries, each with the following keys
stake: :obj:`int` Actual stake in Atto
keys-at-auction: :obj:`list` List of BLS public keys
percentage-of-total-auction-stake: :obj:`str` Percent of total network stake
stake-per-key: :obj:`int` Stake per BLS key in Atto
validator: :obj:`str` Wallet address of validator
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#bef93b3f-6763-4121-9c17-f0b0d9e5cc40
"""
return rpc_request('hmy_getMedianRawStakeSnapshot', endpoint=endpoint, timeout=timeout)['result']
method = 'hmyv2_getMedianRawStakeSnapshot'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e

@ -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

@ -1,7 +1,11 @@
from .rpc.request import (
rpc_request
)
from .exceptions import (
TxConfirmationTimedoutError
)
import time
import random
_default_endpoint = 'http://localhost:9500'
_default_timeout = 30
@ -23,11 +27,152 @@ def get_pending_transactions(endpoint=_default_endpoint, timeout=_default_timeou
Returns
-------
list
# TODO: Add link to reference RPC documentation
list of transactions in the pool, see get_transaction_by_hash for a description
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#de6c4a12-fa42-44e8-972f-801bfde1dd18
"""
method = 'hmyv2_pendingTransactions'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_transaction_error_sink(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get current transactions error sink
Parameters
----------
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
list of transaction failure dictionaries with the following keys:
tx-hash-id: :obj:`str` Transaction hash
time-at-rejection: :obj:`int` Unix time when the transaction was rejected from the pool
error-message: :obj:`str` Reason for transaction rejection (for example insufficient funds)
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#9aedbc22-6262-44b1-8276-cd8ae19fa600
"""
method = 'hmyv2_getCurrentTransactionErrorSink'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_pending_staking_transactions(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Get list of pending staking transactions
Parameters
----------
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
list of staking transactions in the pool, see get_staking_transaction_by_hash for a description
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#de0235e4-f4c9-4a69-b6d2-b77dc1ba7b12
"""
method = 'hmyv2_pendingStakingTransactions'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_staking_transaction_error_sink(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
return rpc_request('hmy_pendingTransactions', endpoint=endpoint, timeout=timeout)['result']
Get current staking transactions error sink
Parameters
----------
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
list of transaction failure dictionaries with the following keys:
tx-hash-id: :obj:`str` Transaction hash
time-at-rejection: :obj:`int` Unix time when the transaction was rejected from the pool
error-message: :obj:`str` Reason for transaction rejection (for example insufficient funds)
directive-kind: :obj:`str` Tope of staking transaction
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#bdd00e0f-2ba0-480e-b996-2ef13f10d75a
"""
method = 'hmyv2_getCurrentStakingErrorSink'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_pool_stats(endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
"""
Get stats of the pool, that is, number of pending and queued (non-executable) transactions
Parameters
----------
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
dict with the following keys:
executable-count: :obj:`int` Number of pending transactions
non-executable-count: :obj:`int` Number of queued (non-executable) transactions
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#7c2b9395-8f5e-4eb5-a687-2f1be683d83e
"""
method = 'hmyv2_getPoolStats'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
####################
# Transaction RPCs #
@ -47,15 +192,45 @@ def get_transaction_by_hash(tx_hash, endpoint=_default_endpoint, timeout=_defaul
Returns
-------
dict
# TODO: Add link to reference RPC documentation
None if transaction hash not found
dict with the following keys
blockHash: :obj:`str` Block hash that transaction was finalized;
"0x0000000000000000000000000000000000000000000000000000000000000000" if tx is pending
blockNumber: :obj:`int` Block number that transaction was finalized; None if tx is pending
ethHash: :obj:`str` legacy from Ethereum; unused
from: :obj:`str` Wallet address
timestamp: :obj:`int` Timestamp in Unix time when transaction was finalized
gas: :obj:`int` Gas limit in Atto
gasPrice :obj:`int` Gas price in Atto
hash: :obj:`str` Transaction hash
input: :obj:`str` Transaction data, used for smart contracts
nonce: :obj:`int` Wallet nonce for the transaction
to: :obj:`str` Wallet address of the receiver
transactionIndex: :obj:`int` Index of transaction in block; None if tx is pending
value: :obj:`int` Amount transferred in Atto
shardID: :obj:`int` Shard where amount if from
toShardID: :obj:`int` Shard where the amount is sent
r: :obj:`str` First 32 bytes of the transaction signature
s: :obj:`str` Next 32 bytes of the transaction signature
v: :obj:`str` Recovery value + 27, as hex string
or None if the transaction is not found
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#117e84f6-a0ec-444e-abe0-455701310389
"""
method = 'hmyv2_getTransactionByHash'
params = [
tx_hash
]
return rpc_request('hmy_getTransactionByHash', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_transaction_by_block_hash_and_index(block_hash, tx_index,
endpoint=_default_endpoint, timeout=_default_timeout
@ -68,7 +243,7 @@ def get_transaction_by_block_hash_and_index(block_hash, tx_index,
block_hash: str
Block hash for transaction
tx_index: int
Transaction index to fetch
Transaction index to fetch (starts from 0)
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -76,15 +251,26 @@ def get_transaction_by_block_hash_and_index(block_hash, tx_index,
Returns
-------
dict
# TODO: Add link to reference RPC documentation
dict, see get_transaction_by_hash for description
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#7c7e8d90-4984-4ebe-bb7e-d7adec167503
"""
method = 'hmyv2_getTransactionByBlockHashAndIndex'
params = [
block_hash,
str(hex(tx_index))
tx_index
]
return rpc_request('hmy_getTransactionByBlockHashAndIndex', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_transaction_by_block_number_and_index(block_num, tx_index,
endpoint=_default_endpoint, timeout=_default_timeout
@ -97,7 +283,7 @@ def get_transaction_by_block_number_and_index(block_num, tx_index,
block_num: int
Block number for transaction
tx_index: int
Transaction index to fetch
Transaction index to fetch (starts from 0)
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -105,23 +291,34 @@ def get_transaction_by_block_number_and_index(block_num, tx_index,
Returns
-------
dict
# TODO: Add link to reference RPC documentation
dict, see get_transaction_by_hash for description
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#bcde8b1c-6ab9-4950-9835-3c7564e49c3e
"""
method = 'hmyv2_getTransactionByBlockNumberAndIndex'
params = [
str(hex(block_num)),
str(hex(tx_index))
block_num,
tx_index
]
return rpc_request('hmy_getTransactionByBlockNumberAndIndex', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_transaction_receipt(tx_receipt, endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
def get_transaction_receipt(tx_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> dict:
"""
Get transaction receipt
Get transaction receipt corresponding to tx_hash
Parameters
----------
tx_receipt: str
tx_hash: str
Transaction receipt to fetch
endpoint: :obj:`str`, optional
Endpoint to send request to
@ -130,22 +327,48 @@ def get_transaction_receipt(tx_receipt, endpoint=_default_endpoint, timeout=_def
Returns
-------
dict
# TODO: Add link to reference RPC documentation
None if transcation receipt hash not found
dict with the following keys:
blockHash: :obj:`str` Block hash
blockNumber: :obj:`int` Block number
contractAddress: :obj:`str` Smart contract address
culmulativeGasUsed: :obj:`int` Gas used for transaction
from: :obj:`str` Sender wallet address
gasUsed: :obj:`int` Gas used for the transaction
logs: :obj:`list` List of logs, each being a dict with keys as follows:
address, blockHash, blockNumber, data, logIndex, removed, topics, transactionHash, transactionIndex
logsBloom :obj:`str` Bloom logs
shardID :obj:`int` Shard ID
status :obj:`int` Status of transaction (0: pending, 1: success)
to :obj:`str` Receiver wallet address
transactionHash :obj:`str` Transaction hash
transactionIndex :obj:`int` Transaction index within block
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#0c2799f8-bcdc-41a4-b362-c3a6a763bb5e
"""
method = 'hmyv2_getTransactionReceipt'
params = [
tx_receipt
tx_hash
]
return rpc_request('hmy_getTransactionReceipt', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_transaction_error_sink(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
def send_raw_transaction(signed_tx, endpoint=_default_endpoint, timeout=_default_timeout) -> str:
"""
Get transaction error sink
Send signed transaction
Parameters
----------
signed_tx: str
Hex representation of signed transaction
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -153,19 +376,36 @@ def get_transaction_error_sink(endpoint=_default_endpoint, timeout=_default_time
Returns
-------
list
# TODO: Add link to reference RPC documentation
"""
return rpc_request('hmy_getCurrentTransactionErrorSink', endpoint=endpoint, timeout=timeout)['result']
str
Transaction hash
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint, or
RPCError
If transaction failed to be added to the pool
def send_raw_transaction(raw_tx, endpoint=_default_endpoint, timeout=_default_timeout) -> str:
API Reference
-------------
https://api.hmny.io/#f40d124a-b897-4b7c-baf3-e0dedf8f40a0
"""
Send signed transaction
params = [
signed_tx
]
method = 'hmyv2_sendRawTransaction'
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def send_and_confirm_raw_transaction(signed_tx, endpoint=_default_endpoint, timeout=_default_timeout) -> list:
"""
Send signed transaction and wait for it to be confirmed
Parameters
----------
raw_tx: str
signed_tx: str
Hex representation of signed transaction
endpoint: :obj:`str`, optional
Endpoint to send request to
@ -175,13 +415,30 @@ def send_raw_transaction(raw_tx, endpoint=_default_endpoint, timeout=_default_ti
Returns
-------
str
Transaction hash
Transaction, see get_transaction_by_hash for structure
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint, or
RPCError
If transaction failed to be added to the pool
TxConfirmationTimedoutError
If transaction could not be confirmed within the timeout period
API Reference
-------------
https://api.hmny.io/#f40d124a-b897-4b7c-baf3-e0dedf8f40a0
"""
params = [
raw_tx
]
return rpc_request('hmy_sendRawTransaction', params=params, endpoint=endpoint, timeout=timeout)['result']
tx_hash = send_raw_transaction(signed_tx)
start_time = time.time()
while((time.time() - start_time) <= timeout):
tx_response = get_transaction_by_hash(tx_hash)
if tx_response is not None:
if tx_response[ 'blockHash' ] != '0x0000000000000000000000000000000000000000000000000000000000000000':
return tx_response
time.sleep(random.uniform(0.2, 0.5))
raise TxConfirmationTimedoutError("Could not confirm transactions on-chain.")
###############################
# CrossShard Transaction RPCs #
@ -199,44 +456,97 @@ def get_pending_cx_receipts(endpoint=_default_endpoint, timeout=_default_timeout
Returns
-------
list
# TODO: Add link to reference RPC documentation
list of CX receipts, each a dict with the following keys
commitBitmap: :obj:`str` Hex represenation of aggregated signature bitmap
commitSig: :obj:`str` Hex representation of aggregated signature
receipts: :obj:`list` list of dictionaries, each representing a cross shard transaction receipt
amount: :obj:`int` Amount in ATTO
from: :obj:`str` From address
to: :obj:`str` From address
shardId: :obj:`int` Originating shard ID
toShardId: :obj:`int` Destination shard ID
txHash: :obj:`str` Transation hash
merkleProof: :obj:`dict` dictionary with the following keys:
blockHash: :obj:`str` Block hash
blockNum: :obj:`int` Block number
receiptHash: :obj:`str` Transaction receipt hash
shardHashes: :obj:`list` Shard hashes for shardIDs
shardID: :obj:`int` Shard ID of originating block
shardIDs: :obj:`list` To shard(s)
header: :obj:`dict` with the following keys (those not noted below are legacy)
shardID: :obj:`int` Originating shard ID
hash: :obj:`str` Block header hash
number: :obj:`int` Block number
viewID: :obj:`int` View ID
epoch: :obj:`int` Epoch number
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint, or
if transaction failed to be added to the pool
API Reference
-------------
https://api.hmny.io/#fe60070d-97b4-458d-9365-490b44c18851
"""
return rpc_request('hmy_getPendingCXReceipts', endpoint=endpoint, timeout=timeout)['result']
method = 'hmyv2_getPendingCXReceipts'
try:
return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_cx_receipt_by_hash(cx_hash, endpoint = _default_endpoint, timeout = _default_timeout) -> dict:
"""
Get cross shard receipt by hash
Get cross shard receipt by hash on the receiving shard end point
Parameters
----------
cx_hash: str
Hash of cross shard transaction receipt
endpoint: :obj:`str`, optional
Endpoint to send request to
Receiving endpoint for the RPC query
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
dict
# TODO: Add link to reference RPC documentation
None if cx receipt hash not found
dict with the following keys
blockHash: :obj:`str` Block hash
blockNumber: :obj:`int` Block number
hash: :obj:`str` Transaction hash
from: :obj:`str` Sender wallet address
to: :obj:`str` Receiver wallet address
shardID: :obj:`int` From shard
toShardID: :obj:`int` To shard
value: :obj:`int` Amount transferred in Atto
None if cx receipt hash not found
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#3d6ad045-800d-4021-aeb5-30a0fbf724fe
"""
params = [
cx_hash
]
return rpc_request('hmy_getCXReceiptByHash', params=params, endpoint=endpoint, timeout=timeout)['result']
method = 'hmyv2_getCXReceiptByHash'
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def resend_cx_receipt(cx_receipt, endpoint=_default_endpoint, timeout=_default_timeout) -> bool:
def resend_cx_receipt(cx_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> bool:
"""
Send cross shard receipt
Resend the cross shard receipt to the receiving shard to re-process if the transaction did not pay out
Parameters
----------
cx_hash: str
cx_hash: :obj:`str`
Hash of cross shard transaction receipt
endpoint: :obj:`str`, optional
Endpoint to send request to
@ -246,13 +556,25 @@ def resend_cx_receipt(cx_receipt, endpoint=_default_endpoint, timeout=_default_t
Returns
-------
bool
If the receipt transactions was succesfully resent
True if the cross shard receipt was succesfully resent
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#c658b56b-d20b-480d-b71a-b0bc505d2164
"""
method = 'hmyv2_resendCx'
params = [
cx_receipt
cx_hash
]
return rpc_request('hmy_resendCx', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
############################
# Staking Transaction RPCs #
@ -272,28 +594,52 @@ def get_staking_transaction_by_hash(tx_hash, endpoint=_default_endpoint, timeout
Returns
-------
dict
# TODO: Add link to reference RPC documentation
None if staking transaction hash not found
dict with the following keys
blockHash: :obj:`str` Block hash in which transaction was finalized
blockNumber: :obj:`int` Block number in which transaction was finalized
from: :obj:`str` Sender wallet address
timestamp: :obj:`int` Unix time at which transaction was finalized
gas: :obj:`int` Gas limit of transaction
gasPrice: :obj:`int` Gas price of transaction in Atto
hash: :obj:`str` Transaction hash
nonce: :obj:`int` Wallet nonce of transaction
transactionIndex: :obj:`int` Staking transaction index within block
type: :obj:`str` Type of staking transaction
msg: :obj:`dict` Staking transaction data, depending on the type of staking transaction
r: :obj:`str` First 32 bytes of the transaction signature
s: :obj:`str` Next 32 bytes of the transaction signature
v: :obj:`str` Recovery value + 27, as hex string
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#296cb4d0-bce2-48e3-bab9-64c3734edd27
"""
method = 'hmyv2_getStakingTransactionByHash'
params = [
tx_hash
]
return rpc_request('hmy_getStakingTransactionByHash', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_staking_transaction_by_block_hash_and_index(block_hash, tx_index,
endpoint=_default_endpoint, timeout=_default_timeout
) -> dict:
"""
Get staking transaction based on index in list of staking transactions for a block by block hash
Get staking transaction by block hash and transaction index
Parameters
----------
block_hash: str
Block hash for staking transaction
Block hash for transaction
tx_index: int
Staking transaction index to fetch
Staking transaction index to fetch (starts at 0)
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -301,26 +647,37 @@ def get_staking_transaction_by_block_hash_and_index(block_hash, tx_index,
Returns
-------
dict
# TODO: Add link to reference RPC documentation
dict, see get_staking_transaction_by_hash for description
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
API Reference
-------------
https://api.hmny.io/#ba96cf61-61fe-464a-aa06-2803bb4b358f
"""
method = 'hmyv2_getStakingTransactionByBlockHashAndIndex'
params = [
block_hash,
str(hex(tx_index))
tx_index
]
return rpc_request('hmy_getStakingTransactionByBlockHashAndIndex', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def get_staking_transaction_by_block_number_and_index(block_num, tx_index,
endpoint=_default_endpoint, timeout=_default_timeout
) -> dict:
"""
Get staking transaction based on index in list of staking transactions for a block by block number
Get staking transaction by block number and transaction index
Parameters
----------
block_num: int
Block number for staking transaction
Block number for transaction
tx_index: int
Staking transaction index to fetch
endpoint: :obj:`str`, optional
@ -330,34 +687,26 @@ def get_staking_transaction_by_block_number_and_index(block_num, tx_index,
Returns
-------
dict
# TODO: Add link to reference RPC documentation
"""
params = [
str(hex(block_num)),
str(hex(tx_index))
]
return rpc_request('hmy_getStakingTransactionByBlockNumberAndIndex', params=params, endpoint=endpoint, timeout=timeout)['result']
dict, see get_staking_transaction_by_hash for description
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint
def get_staking_transaction_error_sink(endpoint=_default_endpoint, timeout=_default_timeout) -> list:
API Reference
-------------
https://api.hmny.io/#fb41d717-1645-4d3e-8071-6ce8e1b65dd3
"""
Get staking transaction error sink
Parameters
----------
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
Timeout in seconds
Returns
-------
list
# TODO: Add link to reference RPC documentation
"""
return rpc_request('hmy_getCurrentStakingErrorSink', endpoint=endpoint, timeout=timeout)['result']
method = 'hmyv2_getStakingTransactionByBlockNumberAndIndex'
params = [
block_num,
tx_index
]
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e
def send_raw_staking_transaction(raw_tx, endpoint=_default_endpoint, timeout=_default_timeout) -> str:
"""
@ -366,7 +715,7 @@ def send_raw_staking_transaction(raw_tx, endpoint=_default_endpoint, timeout=_de
Parameters
----------
raw_tx: str
Hex representation of signed staking transaction
Hex representation of signed transaction
endpoint: :obj:`str`, optional
Endpoint to send request to
timeout: :obj:`int`, optional
@ -375,9 +724,24 @@ def send_raw_staking_transaction(raw_tx, endpoint=_default_endpoint, timeout=_de
Returns
-------
str
Staking transaction hash
Transaction hash
Raises
------
InvalidRPCReplyError
If received unknown result from endpoint, or
RPCError
If transaction failed to be added to the pool
API Reference
-------------
https://api.hmny.io/#e8c17fe9-e730-4c38-95b3-6f1a5b1b9401
"""
method = 'hmyv2_sendRawStakingTransaction'
params = [
raw_tx
]
return rpc_request('hmy_sendRawStakingTransaction', params=params, endpoint=endpoint, timeout=timeout)['result']
try:
return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']
except KeyError as e:
raise InvalidRPCReplyError(method, endpoint) from e

@ -16,8 +16,18 @@ from .rpc.exceptions import (
RequestsTimeoutError,
)
datetime_format = "%Y-%m-%d %H:%M:%S.%f"
from .account import (
is_valid_address
)
from .bech32.bech32 import (
bech32_decode,
convertbits
)
from eth_utils import to_checksum_address
datetime_format = "%Y-%m-%d %H:%M:%S.%f"
class Typgpy(str):
"""
@ -34,6 +44,33 @@ class Typgpy(str):
BOLD = '\033[1m'
UNDERLINE = '\033[4m'
def chain_id_to_int(chainId):
chainIds = dict(
Default = 0,
EthMainnet = 1,
Morden = 2,
Ropsten = 3,
Rinkeby = 4,
RootstockMainnet = 30,
RootstockTestnet = 31,
Kovan = 42,
EtcMainnet = 61,
EtcTestnet = 62,
Geth = 1337,
Ganache = 0,
HmyMainnet = 1,
HmyTestnet = 2,
HmyLocal = 2,
HmyPangaea = 3,
)
if isinstance(chainId, str):
assert chainId in chainIds, f'ChainId {chainId} is not valid'
return chainIds.get(chainId)
elif isinstance(chainId, int):
assert chainId in chainIds.values(), f'Unknown chain id {chainId}'
return chainId
else:
raise TypeError( 'chainId must be str or int' )
def get_gopath():
"""
@ -48,6 +85,17 @@ def get_goversion():
"""
return subprocess.check_output(["go", "version"]).decode().strip()
def convert_one_to_hex(addr):
"""
Given a one address, convert it to hex checksum address
"""
if not is_valid_address(addr):
return to_checksum_address(addr)
hrp, data = bech32_decode(addr)
buf = convertbits(data, 5, 8, False)
address = '0x' + ''.join('{:02x}'.format(x) for x in buf)
return to_checksum_address(address)
def is_active_shard(endpoint, delay_tolerance=60):
"""

@ -1,5 +1,9 @@
import json
from eth_account.datastructures import (
SignedTransaction
)
from decimal import (
Decimal,
InvalidOperation
@ -10,6 +14,10 @@ from .account import (
is_valid_address
)
from .numbers import (
convert_one_to_atto
)
from .exceptions import (
InvalidValidatorError,
RPCError,
@ -17,22 +25,22 @@ from .exceptions import (
RequestsTimeoutError
)
from .numbers import (
convert_atto_to_one,
convert_one_to_atto
)
from .staking import (
get_all_validator_addresses,
get_validator_information
)
from .staking_structures import (
Directive
)
from .staking_signing import (
sign_staking_transaction
)
_default_endpoint = 'http://localhost:9500'
_default_timeout = 30
# TODO: Add validator transaction functions
# TODO: Add unit testing
class Validator:
@ -41,11 +49,11 @@ class Validator:
website_char_limit = 140
security_contact_char_limit = 140
details_char_limit = 280
min_required_delegation = 10000
min_required_delegation = convert_one_to_atto(10000) # in ATTO
def __init__(self, address):
if not isinstance(address, str):
raise InvalidValidatorError(1, f'given ONE address was not a string.')
raise InvalidValidatorError(1, 'given ONE address was not a string')
if not is_valid_address(address):
raise InvalidValidatorError(1, f'{address} is not valid ONE address')
self._address = address
@ -65,6 +73,19 @@ class Validator:
self._max_change_rate = None
self._max_rate = None
def _sanitize_input(self, data, check_str=False) -> str:
"""
If data is None, return '' else return data
Raises
------
InvalidValidatorError if check_str is True and str is not passed
"""
if check_str:
if not isinstance(data, str):
raise InvalidValidatorError(3, f'Expected data to be string to avoid floating point precision issues but got {data}')
return '' if not data else str(data)
def __str__(self) -> str:
"""
Returns JSON string representation of Validator fields
@ -98,6 +119,7 @@ class Validator:
bool
If adding BLS key succeeded
"""
key = self._sanitize_input(key)
if key not in self._bls_keys:
self._bls_keys.append(key)
return True
@ -112,6 +134,7 @@ class Validator:
bool
If removing BLS key succeeded
"""
key = self._sanitize_input(key)
if key in self._bls_keys:
self._bls_keys.remove(key)
return True
@ -124,7 +147,7 @@ class Validator:
Returns
-------
list
List of validator BLS keys
List of validator BLS keys (strings)
"""
return self._bls_keys
@ -142,9 +165,7 @@ class Validator:
InvalidValidatorError
If input is invalid
"""
if not name:
name = ''
name = str(name)
name = self._sanitize_input(name)
if len(name) > self.name_char_limit:
raise InvalidValidatorError(3, f'Name must be less than {self.name_char_limit} characters')
self._name = name
@ -174,9 +195,7 @@ class Validator:
InvalidValidatorError
If input is invalid
"""
if not identity:
identity = ''
identity = str(identity)
identity = self._sanitize_input(identity)
if len(identity) > self.identity_char_limit:
raise InvalidValidatorError(3, f'Identity must be less than {self.identity_char_limit} characters')
self._identity = identity
@ -206,9 +225,7 @@ class Validator:
InvalidValidatorError
If input is invalid
"""
if not website:
website = ''
website = str(website)
website = self._sanitize_input(website)
if len(website) > self.website_char_limit:
raise InvalidValidatorError(3, f'Website must be less than {self.website_char_limit} characters')
self._website = website
@ -238,9 +255,7 @@ class Validator:
InvalidValidatorError
If input is invalid
"""
if not contact:
contact = ''
contact = str(contact)
contact = self._sanitize_input(contact)
if len(contact) > self.security_contact_char_limit:
raise InvalidValidatorError(3, f'Security contact must be less than {self.security_contact_char_limit} characters')
self._security_contact = contact
@ -270,9 +285,7 @@ class Validator:
InvalidValidatorError
If input is invalid
"""
if not details:
details = ''
details = str(details)
details = self._sanitize_input(details)
if len(details) > self.details_char_limit:
raise InvalidValidatorError(3, f'Details must be less than {self.details_char_limit} characters')
self._details = details
@ -294,21 +307,22 @@ class Validator:
Parameters
----------
delegation: str
Minimum self delegation of validator in ONE
delegation: int
Minimum self delegation of validator in ATTO
Raises
------
InvalidValidatorError
If input is invalid
"""
delegation = self._sanitize_input(delegation)
try:
delegation = Decimal(delegation)
except (TypeError, InvalidOperation) as e:
raise InvalidValidatorError(3, f'Min self delegation must be a number') from e
raise InvalidValidatorError(3, 'Min self delegation must be a number') from e
if delegation < self.min_required_delegation:
raise InvalidValidatorError(3, f'Min self delegation must be greater than {self.min_required_delegation} ONE')
self._min_self_delegation = delegation.normalize()
raise InvalidValidatorError(3, f'Min self delegation must be greater than {self.min_required_delegation} ATTO')
self._min_self_delegation = delegation
def get_min_self_delegation(self) -> Decimal:
"""
@ -317,35 +331,36 @@ class Validator:
Returns
-------
Decimal
Validator min self delegation in ONE
Validator min self delegation in ATTO
"""
return self._min_self_delegation
def set_max_total_delegation(self, max):
def set_max_total_delegation(self, max_delegation):
"""
Set validator max total delegation
Parameters
----------
max: str
Maximum total delegation of validator in ONE
max_delegation: int
Maximum total delegation of validator in ATTO
Raises
------
InvalidValidatorError
If input is invalid
"""
max_delegation = self._sanitize_input(max_delegation)
try:
max = Decimal(max)
max_delegation = Decimal(max_delegation)
except (TypeError, InvalidOperation) as e:
raise InvalidValidatorError(3, 'Max total delegation must be a number') from e
if self._min_self_delegation:
if max < self._min_self_delegation:
if max_delegation < self._min_self_delegation:
raise InvalidValidatorError(3, f'Max total delegation must be greater than min self delegation: '
f'{self._min_self_delegation}')
'{self._min_self_delegation}')
else:
raise InvalidValidatorError(4, 'Min self delegation must be set before max total delegation')
self._max_total_delegation = max.normalize()
self._max_total_delegation = max_delegation
def get_max_total_delegation(self) -> Decimal:
"""
@ -354,7 +369,7 @@ class Validator:
Returns
-------
Decimal
Validator max total delegation in ONE
Validator max total delegation in ATTO
"""
return self._max_total_delegation
@ -365,30 +380,31 @@ class Validator:
Parameters
----------
amount: str
Initial delegation amount of validator in ONE
Initial delegation amount of validator in ATTO
Raises
------
InvalidValidatorError
If input is invalid
"""
amount = self._sanitize_input(amount)
try:
amount = Decimal(amount)
except (TypeError, InvalidOperation) as e:
raise InvalidValidatorError(3, f'Amount must be a number') from e
raise InvalidValidatorError(3, 'Amount must be a number') from e
if self._min_self_delegation:
if amount < self._min_self_delegation:
raise InvalidValidatorError(3, f'Amount must be greater than min self delegation: '
raise InvalidValidatorError(3, 'Amount must be greater than min self delegation: '
f'{self._min_self_delegation}')
else:
raise InvalidValidatorError(4, f'Min self delegation must be set before amount')
raise InvalidValidatorError(4, 'Min self delegation must be set before amount')
if self._max_total_delegation:
if amount > self._max_total_delegation:
raise InvalidValidatorError(3, f'Amount must be less than max total delegation: '
raise InvalidValidatorError(3, 'Amount must be less than max total delegation: '
f'{self._max_total_delegation}')
else:
raise InvalidValidatorError(4, f'Max total delegation must be set before amount')
self._inital_delegation = amount.normalize()
raise InvalidValidatorError(4, 'Max total delegation must be set before amount')
self._inital_delegation = amount
def get_amount(self) -> Decimal:
"""
@ -397,7 +413,7 @@ class Validator:
Returns
-------
Decimal
Intended initial delegation amount in ONE
Intended initial delegation amount in ATTO
"""
return self._inital_delegation
@ -407,7 +423,7 @@ class Validator:
Parameters
----------
rate: str
rate: str (to avoid precision troubles)
Max commission rate of validator
Raises
@ -415,13 +431,14 @@ class Validator:
InvalidValidatorError
If input is invalid
"""
rate = self._sanitize_input(rate, True)
try:
rate = Decimal(rate)
except (TypeError, InvalidOperation) as e:
raise InvalidValidatorError(3, f'Max rate must be a number') from e
raise InvalidValidatorError(3, 'Max rate must be a number') from e
if rate < 0 or rate > 1:
raise InvalidValidatorError(3, f'Max rate must be between 0 and 1')
self._max_rate = rate.normalize()
raise InvalidValidatorError(3, 'Max rate must be between 0 and 1')
self._max_rate = rate
def get_max_rate(self) -> Decimal:
"""
@ -440,7 +457,7 @@ class Validator:
Parameters
----------
rate: str
rate: str (to avoid precision troubles)
Max commission change rate of validator
Raises
@ -448,18 +465,19 @@ class Validator:
InvalidValidatorError
If input is invalid
"""
rate = self._sanitize_input(rate, True)
try:
rate = Decimal(rate)
except (TypeError, InvalidOperation) as e:
raise InvalidValidatorError(3, f'Max change rate must be a number') from e
raise InvalidValidatorError(3, 'Max change rate must be a number') from e
if rate < 0:
raise InvalidValidatorError(3, f'Max change rate must be greater than or equal to 0')
raise InvalidValidatorError(3, 'Max change rate must be greater than or equal to 0')
if self._max_rate:
if rate > self._max_rate:
raise InvalidValidatorError(3, f'Max change rate must be less than or equal to max rate: {self._max_rate}')
else:
raise InvalidValidatorError(4, f'Max rate must be set before max change rate')
self._max_change_rate = rate.normalize()
raise InvalidValidatorError(4, 'Max rate must be set before max change rate')
self._max_change_rate = rate
def get_max_change_rate(self) -> Decimal:
"""
@ -467,7 +485,7 @@ class Validator:
Returns
-------
Decimal
Decimal (to avoid precision troubles)
Validator max change rate
"""
return self._max_change_rate
@ -478,7 +496,7 @@ class Validator:
Parameters
----------
rate: str
rate: str (to avoid precision troubles)
Commission rate of validator
Raises
@ -486,18 +504,19 @@ class Validator:
InvalidValidatorError
If input is invalid
"""
rate = self._sanitize_input(rate, True)
try:
rate = Decimal(rate)
except (TypeError, InvalidOperation) as e:
raise InvalidValidatorError(3, f'Rate must be a number') from e
raise InvalidValidatorError(3, 'Rate must be a number') from e
if rate < 0:
raise InvalidValidatorError(3, f'Rate must be greater than or equal to 0')
raise InvalidValidatorError(3, 'Rate must be greater than or equal to 0')
if self._max_rate:
if rate > self._max_rate:
raise InvalidValidatorError(3, f'Rate must be less than or equal to max rate: {self._max_rate}')
else:
raise InvalidValidatorError(4, f'Max rate must be set before rate')
self._rate = rate.normalize()
raise InvalidValidatorError(4, 'Max rate must be set before rate')
self._rate = rate
def get_rate(self) -> Decimal:
"""
@ -555,9 +574,10 @@ class Validator:
"amount": 0,
"min-self-delegation": 0,
"max-total-delegation": 0,
"rate": 0,
"max-rate": 0,
"max-change-rate": 0
"rate": '0',
"max-rate": '0',
"max-change-rate": '0',
"bls-public-keys": [ "" ]
}
Raises
@ -565,19 +585,26 @@ class Validator:
InvalidValidatorError
If input value is invalid
"""
self.set_name(info['name'])
self.set_identity(info['identity'])
self.set_website(info['website'])
self.set_details(info['details'])
self.set_security_contact(info['security-contact'])
self.set_min_self_delegation(info['min-self-delegation'])
self.set_max_total_delegation(info['max-total-delegation'])
self.set_amount(info['amount'])
self.set_max_rate(info['max-rate'])
self.set_max_change_rate(info['max-change-rate'])
self.set_rate(info['rate'])
try:
self.set_name(info['name'])
self.set_identity(info['identity'])
self.set_website(info['website'])
self.set_details(info['details'])
self.set_security_contact(info['security-contact'])
self.set_min_self_delegation(info['min-self-delegation'])
self.set_max_total_delegation(info['max-total-delegation'])
self.set_amount(info['amount'])
self.set_max_rate(info['max-rate'])
self.set_max_change_rate(info['max-change-rate'])
self.set_rate(info['rate'])
self._bls_keys = []
for key in info['bls-public-keys']:
self.add_bls_key(key)
except KeyError as e:
raise InvalidValidatorError(3, 'Info has missing key') from e
def load_from_blockchain(self, endpoint=_default_endpoint, timeout=_default_timeout):
"""
@ -599,11 +626,11 @@ class Validator:
if not self.does_validator_exist(endpoint, timeout):
raise InvalidValidatorError(5, f'Validator does not exist on chain according to {endpoint}')
except (RPCError, RequestsError, RequestsTimeoutError) as e:
raise InvalidValidatorError(5, f'Error requesting validator information') from e
raise InvalidValidatorError(5, 'Error requesting validator information') from e
try:
validator_info = get_validator_information(self._address, endpoint, timeout)
except (RPCError, RequestsError, RequestsTimeoutError) as e:
raise InvalidValidatorError(5, f'Error requesting validator information') from e
raise InvalidValidatorError(5, 'Error requesting validator information') from e
# Skip additional sanity checks when importing from chain
try:
@ -614,15 +641,16 @@ class Validator:
self._details = info['details']
self._security_contact = info['security-contact']
self._min_self_delegation = convert_atto_to_one(info['min-self-delegation']).normalize()
self._max_total_delegation = convert_atto_to_one(info['max-total-delegation']).normalize()
self._min_self_delegation = info['min-self-delegation']
self._max_total_delegation = info['max-total-delegation']
self._inital_delegation = self._min_self_delegation # Since validator exists, set initial delegation to 0
self._max_rate = Decimal(info['max-rate']).normalize()
self._max_change_rate = Decimal(info['max-change-rate']).normalize()
self._rate = Decimal(info['rate']).normalize()
self._max_rate = Decimal(info['max-rate'])
self._max_change_rate = Decimal(info['max-change-rate'])
self._rate = Decimal(info['rate'])
self._bls_keys = info[ 'bls-public-keys' ]
except KeyError as e:
raise InvalidValidatorError(5, f'Error importing validator information from RPC result') from e
raise InvalidValidatorError(5, 'Error importing validator information from RPC result') from e
def export(self) -> dict:
"""
@ -645,6 +673,70 @@ class Validator:
"max-total-delegation": self._max_total_delegation,
"rate": self._rate,
"max-rate": self._max_rate,
"max-change-rate": self._max_change_rate
"max-change-rate": self._max_change_rate,
"bls-public-keys": self._bls_keys
}
return info
def sign_create_validator_transaction(self, nonce, gas_price, gas_limit, private_key, chain_id=None) -> SignedTransaction:
"""
Create but not post a transaction to Create the Validator using private_key
Returns
-------
SignedTransaction object, the hash of which can be used to send the transaction
using transaction.send_raw_transaction
Raises
------
rlp.exceptions.ObjectSerializationError for malformed inputs
API Reference
-------------
https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L413
"""
info = self.export().copy()
info['directive'] = Directive.CreateValidator
info['validatorAddress'] = info.pop('validator-addr') # change the key
info['nonce'] = nonce
info['gasPrice'] = gas_price
info['gasLimit'] = gas_limit
if chain_id:
info['chainId'] = chain_id
return sign_staking_transaction(info, private_key)
def sign_edit_validator_transaction(self, nonce, gas_price, gas_limit, rate, bls_key_to_add, bls_key_to_remove, private_key, chain_id=None) -> SignedTransaction:
"""
Create but not post a transaction to Edit the Validator using private_key
Returns
-------
SignedTransaction object, the hash of which can be used to send the transaction
using transaction.send_raw_transaction
Raises
------
rlp.exceptions.ObjectSerializationError for malformed inputs
API Reference
-------------
https://github.com/harmony-one/sdk/blob/99a827782fabcd5f91f025af0d8de228956d42b4/packages/harmony-staking/src/stakingTransaction.ts#L460
"""
self.set_rate(rate)
self.add_bls_key(bls_key_to_add)
self.remove_bls_key(bls_key_to_remove)
info = self.export().copy()
info['directive'] = Directive.EditValidator
info['validatorAddress'] = info.pop('validator-addr') # change the key
info['nonce'] = nonce
info['gasPrice'] = gas_price
info['gasLimit'] = gas_limit
_ = info.pop('max-rate') # not needed
_ = info.pop('max-change-rate') # not needed
_ = info.pop('bls-public-keys') # remove this list
_ = info.pop('amount') # also unused
info['bls-key-to-remove'] = bls_key_to_remove
info['bls-key-to-add'] = bls_key_to_add
if chain_id:
info['chainId'] = chain_id
return sign_staking_transaction(info, private_key)

@ -20,13 +20,18 @@ setup(
'pexpect',
'requests',
'incremental',
'eth-rlp',
'eth-account',
'eth-utils',
'hexbytes',
'cytoolz'
],
setup_requires=[
'incremental',
'pytest',
'pytest-ordering',
'click',
'twisted'
'twisted',
],
classifiers=[
'Development Status :: 3 - Alpha',

@ -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')

@ -14,7 +14,7 @@ from pyhmy.rpc import (
def setup():
endpoint = 'http://localhost:9500'
timeout = 30
method = 'hmy_getNodeMetadata'
method = 'hmyv2_getNodeMetadata'
params = []
payload = {
"id": "1",
@ -46,7 +46,7 @@ def test_request_connection_error():
bad_endpoint = f'http://localhost:{port}'
bad_request = None
try:
bad_request = request.rpc_request('hmy_getNodeMetadata', endpoint=bad_endpoint)
bad_request = request.rpc_request('hmyv2_getNodeMetadata', endpoint=bad_endpoint)
except Exception as e:
assert isinstance(e, exceptions.RequestsError)
assert bad_request is None
@ -56,7 +56,7 @@ def test_request_connection_error():
def test_request_rpc_error():
error_request = None
try:
error_request = request.rpc_request('hmy_getBalance')
error_request = request.rpc_request('hmyv2_getBalance')
except (exceptions.RequestsTimeoutError, exceptions.RequestsError) as err:
pytest.skip("can not connect to local blockchain", allow_module_level=True)
except Exception as e:
@ -68,7 +68,7 @@ def test_request_rpc_error():
def test_rpc_request():
endpoint = 'http://localhost:9500'
timeout = 30
method = 'hmy_getNodeMetadata'
method = 'hmyv2_getNodeMetadata'
params = []
payload = {
"id": "1",

@ -9,6 +9,8 @@ transfer_raw_transaction = '0xf86f80843b9aca008252080180943ad89a684095a53edb47d7
tx_hash = '0x1fa20537ea97f162279743139197ecf0eac863278ac1c8ada9a6be5d1e31e633'
create_validator_raw_transaction = '0xf9015680f90105943ad89a684095a53edb47d7ddc5e034d813366731d984746573748474657374847465737484746573748474657374ddc988016345785d8a0000c9880c7d713b49da0000c887b1a2bc2ec500008a022385a827e8155000008b084595161401484a000000f1b0282554f2478661b4844a05a9deb1837aac83931029cb282872f0dcd7239297c499c02ea8da8746d2f08ca2b037e89891f862b86003557e18435c201ecc10b1664d1aea5b4ec59dbfe237233b953dbd9021b86bc9770e116ed3c413fe0334d89562568a10e133d828611f29fee8cdab9719919bbcc1f1bf812c73b9ccd0f89b4f0b9ca7e27e66d58bbb06fcf51c295b1d076cfc878a0228f16f86157860000080843b9aca008351220027a018385211a150ca032c3526cef0aba6a75f99a18cb73f547f67bab746be0c7a64a028be921002c6eb949b3932afd010dfe1de2459ec7fe84403b9d9d8892394a78c'
staking_tx_hash = '0x57ec011aabdeb078a4816502224022f291fa8b07c82bbae8476f514a1d71c730'
contract_tx_hash = '0xa13414dd152173395c69a11e79dea31bf029660f747a42a53744181d05571e70'
contract_raw_transaction = '0xf9025080843b9aca008366916c80808080b901fc608060405234801561001057600080fd5b50336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555061019c806100606000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c8063445df0ac146100465780638da5cb5b14610064578063fdacd576146100ae575b600080fd5b61004e6100dc565b6040518082815260200191505060405180910390f35b61006c6100e2565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b6100da600480360360208110156100c457600080fd5b8101908080359060200190929190505050610107565b005b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff16141561016457806001819055505b5056fea265627a7a723158209b80813a158b44af65aee232b44c0ac06472c48f4abbe298852a39f0ff34a9f264736f6c6343000510003227a03a3ad2b7c2934a8325fc04d04daad740d337bb1f589482bbb1d091e1be804d29a00c46772871866a34f254e6197a526bebc2067f75edc53c488b31d84e07c3c685'
endpoint = 'http://localhost:9500'
endpoint_shard_one = 'http://localhost:9501'
@ -31,7 +33,7 @@ def setup_blockchain():
tx_data = _check_funding_transaction()
if 'error' in tx_data:
pytest.skip(f"Error in hmy_getTransactionByHash reply: {tx_data['error']}", allow_module_level=True)
pytest.skip(f"Error in hmyv2_getTransactionByHash reply: {tx_data['error']}", allow_module_level=True)
if not tx_data['result']:
pytest.skip(f"Funding transaction failed: {tx_hash}", allow_module_level=True)
@ -44,15 +46,27 @@ def setup_blockchain():
stx_data = _check_staking_transaction()
if 'error' in stx_data:
pytest.skip(f"Error in hmy_getStakingTransactionByHash reply: {stx_data['error']}", allow_module_level=True)
pytest.skip(f"Error in hmyv2_getStakingTransactionByHash reply: {stx_data['error']}", allow_module_level=True)
if not stx_data['result']:
pytest.skip(f"Staking transaction failed: {staking_tx_hash}", allow_module_level=True)
contract_data = _check_contract_transaction()
if not contract_data['result']:
_send_contract_transaction()
times.sleep(30)
contract_data = _check_contract_transaction()
if 'error' in contract_data:
pytest.skip(f"Error in hmyv2_getStakingTransactionByHash reply: {contract_data['error']}", allow_module_level=True)
if not contract_data['result']:
pytest.skip(f"Staking transaction failed: {contract_tx_hash}", allow_module_level=True)
# TODO: Build data object to return data instead of hard coded values in the test files
try:
return int(stx_data['result']['blockNumber'], 16)
return int(stx_data['result']['blockNumber'])
except (TypeError, KeyError) as e:
pytest.skip(f"Unexpected reply for hmy_getStakingTransactionByHash: {stx_data['result']}", allow_module_level=True)
pytest.skip(f"Unexpected reply for hmyv2_getStakingTransactionByHash: {stx_data['result']}", allow_module_level=True)
def _check_connection():
@ -60,19 +74,19 @@ def _check_connection():
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmy_getNodeMetadata',
"method": 'hmyv2_getNodeMetadata',
"params": []
}
response = requests.request('POST', endpoint, headers=headers,
data=json.dumps(payload), timeout=timeout, allow_redirects=True)
metadata = json.loads(response.content)
if 'error' in metadata:
pytest.skip(f"Error in hmy_getNodeMetadata reply: {metadata['error']}", allow_module_level=True)
pytest.skip(f"Error in hmyv2_getNodeMetadata reply: {metadata['error']}", allow_module_level=True)
if 'chain-config' not in metadata['result']:
pytest.skip("Chain config not found in hmy_getNodeMetadata reply", allow_module_level=True)
pytest.skip("Chain config not found in hmyv2_getNodeMetadata reply", allow_module_level=True)
return metadata
except Exception as e:
pytest.skip('Can not connect to local blockchain or bad hmy_getNodeMetadata reply', allow_module_level=True)
pytest.skip('Can not connect to local blockchain or bad hmyv2_getNodeMetadata reply', allow_module_level=True)
def _check_staking_epoch(metadata):
latest_header = None
@ -80,16 +94,16 @@ def _check_staking_epoch(metadata):
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmy_latestHeader',
"method": 'hmyv2_latestHeader',
"params": []
}
response = requests.request('POST', endpoint, headers=headers,
data=json.dumps(payload), timeout=timeout, allow_redirects=True)
latest_header = json.loads(response.content)
if 'error' in latest_header:
pytest.skip(f"Error in hmy_latestHeader reply: {latest_header['error']}", allow_module_level=True)
pytest.skip(f"Error in hmyv2_latestHeader reply: {latest_header['error']}", allow_module_level=True)
except Exception as e:
pytest.skip('Failed to get hmy_latestHeader reply', allow_module_level=True)
pytest.skip('Failed to get hmyv2_latestHeader reply', allow_module_level=True)
if metadata and latest_header:
staking_epoch = metadata['result']['chain-config']['staking-epoch']
@ -102,23 +116,23 @@ def _send_funding_transaction():
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmy_sendRawTransaction',
"method": 'hmyv2_sendRawTransaction',
"params": [transfer_raw_transaction]
}
response = requests.request('POST', endpoint_shard_one, headers=headers,
data=json.dumps(payload), timeout=timeout, allow_redirects=True)
tx = json.loads(response.content)
if 'error' in tx:
pytest.skip(f"Error in hmy_sendRawTransaction reply: {tx['error']}", allow_module_level=True)
pytest.skip(f"Error in hmyv2_sendRawTransaction reply: {tx['error']}", allow_module_level=True)
except Exception as e:
pytest.skip('Failed to get hmy_sendRawTransaction reply', allow_module_level=True)
pytest.skip('Failed to get hmyv2_sendRawTransaction reply', allow_module_level=True)
def _check_funding_transaction():
try:
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmy_getTransactionByHash',
"method": 'hmyv2_getTransactionByHash',
"params": [tx_hash]
}
response = requests.request('POST', endpoint_shard_one, headers=headers,
@ -126,30 +140,61 @@ def _check_funding_transaction():
tx_data = json.loads(response.content)
return tx_data
except Exception as e:
pytest.skip('Failed to get hmy_getTransactionByHash reply', allow_module_level=True)
pytest.skip('Failed to get hmyv2_getTransactionByHash reply', allow_module_level=True)
def _check_contract_transaction():
try:
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmyv2_getTransactionByHash',
"params": [contract_tx_hash]
}
response = requests.request('POST', endpoint, headers=headers,
data=json.dumps(payload), timeout=timeout, allow_redirects=True)
tx_data = json.loads(response.content)
return tx_data
except Exception as e:
pytest.skip('Failed to get hmyv2_getTransactionByHash reply', allow_module_level=True)
def _send_contract_transaction():
try:
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmyv2_sendRawTransaction',
"params": [contract_raw_transaction]
}
response = requests.request('POST', endpoint, headers=headers,
data=json.dumps(payload), timeout=timeout, allow_redirects=True)
tx_data = json.loads(response.content)
if 'error' in staking_tx:
pytest.skip(f"Error in hmyv2_sendRawTransaction reply: {tx_data['error']}", allow_module_level=True)
except Exception as e:
pytest.skip('Failed to get hmyv2_sendRawTransaction reply', allow_module_level=True)
def _send_staking_transaction():
try:
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmy_sendRawStakingTransaction',
"method": 'hmyv2_sendRawStakingTransaction',
"params": [create_validator_raw_transaction]
}
response = requests.request('POST', endpoint, headers=headers,
data=json.dumps(payload), timeout=timeout, allow_redirects=True)
staking_tx = json.loads(response.content)
if 'error' in staking_tx:
pytest.skip(f"Error in hmy_sendRawStakingTransaction reply: {staking_tx['error']}", allow_module_level=True)
pytest.skip(f"Error in hmyv2_sendRawStakingTransaction reply: {staking_tx['error']}", allow_module_level=True)
except Exception as e:
pytest.skip('Failed to get hmy_sendRawStakingTransaction reply', allow_module_level=True)
pytest.skip('Failed to get hmyv2_sendRawStakingTransaction reply', allow_module_level=True)
def _check_staking_transaction():
try:
payload = {
"id": "1",
"jsonrpc": "2.0",
"method": 'hmy_getStakingTransactionByHash',
"method": 'hmyv2_getStakingTransactionByHash',
"params": [staking_tx_hash]
}
response = requests.request('POST', endpoint, headers=headers,
@ -157,4 +202,4 @@ def _check_staking_transaction():
stx_data = json.loads(response.content)
return stx_data
except Exception as e:
pytest.skip('Failed to get hmy_getStakingTransactionByHash reply', allow_module_level=True)
pytest.skip('Failed to get hmyv2_getStakingTransactionByHash reply', allow_module_level=True)

@ -16,6 +16,7 @@ local_test_address = 'one1zksj3evekayy90xt4psrz8h6j2v3hla4qwz4ur'
test_validator_address = 'one18tvf56zqjkjnak686lwutcp5mqfnvee35xjnhc'
genesis_block_number = 0
test_block_number = 1
fake_shard = 'http://example.com'
def _test_account_rpc(fn, *args, **kwargs):
if not callable(fn):
@ -43,36 +44,29 @@ def test_get_balance_by_block(setup_blockchain):
assert balance > 0
@pytest.mark.run(order=3)
def test_get_true_nonce(setup_blockchain):
true_nonce = _test_account_rpc(account.get_account_nonce, local_test_address, true_nonce=True, endpoint=endpoint_shard_one)
def test_get_account_nonce(setup_blockchain):
true_nonce = _test_account_rpc(account.get_account_nonce, local_test_address, test_block_number, endpoint=endpoint_shard_one)
assert isinstance(true_nonce, int)
assert true_nonce > 0
@pytest.mark.run(order=4)
def test_get_pending_nonce(setup_blockchain):
pending_nonce = _test_account_rpc(account.get_account_nonce, local_test_address, endpoint=endpoint_shard_one)
assert isinstance(pending_nonce, int)
assert pending_nonce > 0
@pytest.mark.run(order=5)
def test_get_transaction_history(setup_blockchain):
tx_history = _test_account_rpc(account.get_transaction_history, local_test_address, endpoint=explorer_endpoint)
assert isinstance(tx_history, list)
assert len(tx_history) >= 0
@pytest.mark.run(order=6)
@pytest.mark.run(order=5)
def test_get_staking_transaction_history(setup_blockchain):
staking_tx_history = _test_account_rpc(account.get_staking_transaction_history, test_validator_address, endpoint=explorer_endpoint)
assert isinstance(staking_tx_history, list)
assert len(staking_tx_history) > 0
@pytest.mark.run(order=7)
@pytest.mark.run(order=6)
def test_get_balance_on_all_shards(setup_blockchain):
balances = _test_account_rpc(account.get_balance_on_all_shards, local_test_address)
assert isinstance(balances, list)
assert len(balances) == 2
@pytest.mark.run(order=8)
@pytest.mark.run(order=7)
def test_get_total_balance(setup_blockchain):
total_balance = _test_account_rpc(account.get_total_balance, local_test_address)
assert isinstance(total_balance, int)
@ -82,3 +76,41 @@ def test_get_total_balance(setup_blockchain):
def test_is_valid_address():
assert account.is_valid_address('one1zksj3evekayy90xt4psrz8h6j2v3hla4qwz4ur')
assert not account.is_valid_address('one1wje75aedczmj4dwjs0812xcg7vx0dy231cajk0')
@pytest.mark.run(order=8)
def test_get_transaction_count(setup_blockchain):
tx_count = _test_account_rpc(account.get_transaction_count, local_test_address, 'latest')
assert isinstance(tx_count, int)
assert tx_count > 0
@pytest.mark.run(order=9)
def test_get_transactions_count(setup_blockchain):
tx_count = _test_account_rpc(account.get_transactions_count, local_test_address, 'ALL')
@pytest.mark.run(order=10)
def test_get_staking_transactions_count(setup_blockchain):
tx_count = _test_account_rpc(account.get_staking_transactions_count, local_test_address, 'ALL')
assert isinstance(tx_count, int)
@pytest.mark.run(order=10)
def test_errors():
with pytest.raises(exceptions.RPCError):
account.get_balance('', fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_balance_by_block('', 1, fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_account_nonce('', 1, fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_transaction_count('', 1, fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_transactions_count('', 1, fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_transactions_count('', 'ALL', fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_transaction_history('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_staking_transaction_history('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_balance_on_all_shards('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
account.get_total_balance('', endpoint=fake_shard)

@ -14,6 +14,7 @@ test_epoch_number = 0
genesis_block_number = 0
test_block_number = 1
test_block_hash = None
fake_shard = 'http://example.com'
def _test_blockchain_rpc(fn, *args, **kwargs):
if not callable(fn):
@ -70,8 +71,8 @@ def test_get_latest_header(setup_blockchain):
assert isinstance(header, dict)
@pytest.mark.run(order=9)
def test_get_latest_headers(setup_blockchain):
header_pair = _test_blockchain_rpc(blockchain.get_latest_headers)
def test_get_latest_chain_headers(setup_blockchain):
header_pair = _test_blockchain_rpc(blockchain.get_latest_chain_headers)
assert isinstance(header_pair, dict)
@pytest.mark.run(order=10)
@ -139,7 +140,7 @@ def test_get_prestaking_epoch(setup_blockchain):
@pytest.mark.run(order=20)
def test_get_bad_blocks(setup_blockchain):
# TODO: Remove skip when RPC is fixed
pytest.skip("Known error with hmy_getCurrentBadBlocks")
pytest.skip("Known error with hmyv2_getCurrentBadBlocks")
bad_blocks = _test_blockchain_rpc(blockchain.get_bad_blocks)
assert isinstance(bad_blocks, list)
@ -150,7 +151,171 @@ def test_get_validator_keys(setup_blockchain):
assert len(keys) > 0
@pytest.mark.run(order=22)
def test_get_block_signer_keys(setup_blockchain):
keys = _test_blockchain_rpc(blockchain.get_block_signer_keys, test_block_number)
def test_get_block_signers_keys(setup_blockchain):
keys = _test_blockchain_rpc(blockchain.get_block_signers_keys, test_block_number)
assert isinstance(keys, list)
assert len(keys) > 0
@pytest.mark.run(order=23)
def test_chain_id(setup_blockchain):
chain_id = _test_blockchain_rpc(blockchain.chain_id)
assert isinstance(chain_id, int)
@pytest.mark.run(order=24)
def test_get_peer_info(setup_blockchain):
peer_info = _test_blockchain_rpc(blockchain.get_peer_info)
assert isinstance(peer_info, dict)
@pytest.mark.run(order=25)
def test_protocol_version(setup_blockchain):
protocol_version = _test_blockchain_rpc(blockchain.protocol_version)
assert isinstance(protocol_version, int)
@pytest.mark.run(order=26)
def test_is_last_block(setup_blockchain):
is_last_block = _test_blockchain_rpc(blockchain.is_last_block, 0)
assert isinstance(is_last_block, bool)
assert not is_last_block
@pytest.mark.run(order=27)
def test_epoch_last_block(setup_blockchain):
epoch_last_block = _test_blockchain_rpc(blockchain.epoch_last_block, 0)
assert isinstance(epoch_last_block, int)
@pytest.mark.run(order=28)
def test_get_circulating_supply(setup_blockchain):
circulating_supply = _test_blockchain_rpc(blockchain.get_circulating_supply)
assert isinstance(circulating_supply, str)
@pytest.mark.run(order=29)
def test_get_total_supply(setup_blockchain):
total_supply = _test_blockchain_rpc(blockchain.get_total_supply)
assert isinstance(total_supply, str) or total_supply == None
@pytest.mark.run(order=30)
def test_get_last_cross_links(setup_blockchain):
last_cross_links = _test_blockchain_rpc(blockchain.get_last_cross_links)
assert isinstance(last_cross_links, list)
@pytest.mark.run(order=31)
def test_get_gas_price(setup_blockchain):
gas_price = _test_blockchain_rpc(blockchain.get_gas_price)
assert isinstance(gas_price, int)
@pytest.mark.run(order=32)
def test_get_version(setup_blockchain):
version = _test_blockchain_rpc(blockchain.get_version)
assert isinstance(version, int)
@pytest.mark.run(order=33)
def test_get_header_by_number(setup_blockchain):
header_pair = _test_blockchain_rpc(blockchain.get_header_by_number, 0)
assert isinstance(header_pair, dict)
@pytest.mark.run(order=34)
def test_get_block_staking_transaction_count_by_number(setup_blockchain):
tx_count = _test_blockchain_rpc(blockchain.get_block_staking_transaction_count_by_number, test_block_number)
assert isinstance(tx_count, int)
@pytest.mark.run(order=35)
def test_get_block_staking_transaction_count_by_hash(setup_blockchain):
if not test_block_hash:
pytest.skip('Failed to get reference block hash')
tx_count = _test_blockchain_rpc(blockchain.get_block_staking_transaction_count_by_hash, test_block_hash)
assert isinstance(tx_count, int)
@pytest.mark.run(order=36)
def test_is_block_signer(setup_blockchain):
is_signer = _test_blockchain_rpc(blockchain.is_block_signer, test_block_number, '0x0')
assert isinstance(is_signer, bool)
@pytest.mark.run(order=37)
def test_get_signed_blocks(setup_blockchain):
signed_blocks = _test_blockchain_rpc(blockchain.get_signed_blocks, '0x0')
assert isinstance(signed_blocks, int)
@pytest.mark.run(order=38)
def test_in_sync(setup_blockchain):
in_sync = _test_blockchain_rpc(blockchain.in_sync)
assert isinstance(in_sync, bool)
@pytest.mark.run(order=38)
def test_beacon_in_sync(setup_blockchain):
beacon_in_sync = _test_blockchain_rpc(blockchain.beacon_in_sync)
assert isinstance(beacon_in_sync, bool)
def test_errors():
with pytest.raises(exceptions.RPCError):
blockchain.chain_id(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_node_metadata(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_peer_info(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.protocol_version(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_shard(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_staking_epoch(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_prestaking_epoch(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_sharding_structure(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_leader_address(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.is_last_block(0, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.epoch_last_block(0, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_circulating_supply(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_total_supply(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_number(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_current_epoch(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_last_cross_links(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_gas_price(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_num_peers(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_version(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_latest_header(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_header_by_number(0, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_latest_chain_headers(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_by_number(0, endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_by_hash('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_transaction_count_by_number(0, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_transaction_count_by_hash('', fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_staking_transaction_count_by_number(0, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_staking_transaction_count_by_hash('', fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_blocks(0, 1, endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_signers(0, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_block_signers_keys(0, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.is_block_signer(0, '', fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_signed_blocks('', fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_validators(1, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.get_validator_keys(0, fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.in_sync(fake_shard)
with pytest.raises(exceptions.RPCError):
blockchain.beacon_in_sync(fake_shard)

@ -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'

@ -12,6 +12,7 @@ from pyhmy.rpc import (
explorer_endpoint = 'http://localhost:9599'
test_validator_address = 'one18tvf56zqjkjnak686lwutcp5mqfnvee35xjnhc'
fake_shard = 'http://example.com'
def _test_staking_rpc(fn, *args, **kwargs):
if not callable(fn):
@ -78,16 +79,113 @@ def test_get_raw_median_stake_snapshot(setup_blockchain):
@pytest.mark.run(order=10)
def test_get_validator_information_by_block(setup_blockchain):
# Apparently validator information not created until block after create-validator transaction is accepted, so +1 block
info = _test_staking_rpc(staking.get_validator_information_by_block, test_validator_address, setup_blockchain + 1, endpoint=explorer_endpoint)
info = _test_staking_rpc(staking.get_validator_information_by_block_number, test_validator_address, setup_blockchain + 1, endpoint=explorer_endpoint)
assert isinstance(info, dict)
@pytest.mark.run(order=11)
def test_get_validator_information_by_block(setup_blockchain):
# Apparently validator information not created until block after create-validator transaction is accepted, so +1 block
info = _test_staking_rpc(staking.get_all_validator_information_by_block, setup_blockchain + 1, endpoint=explorer_endpoint)
info = _test_staking_rpc(staking.get_all_validator_information_by_block_number, setup_blockchain + 1, endpoint=explorer_endpoint)
assert isinstance(info, list)
@pytest.mark.run(order=12)
def test_get_delegations_by_delegator_by_block(setup_blockchain):
delegations = _test_staking_rpc(staking.get_delegations_by_delegator_by_block, test_validator_address, setup_blockchain + 1, endpoint=explorer_endpoint)
delegations = _test_staking_rpc(staking.get_delegations_by_delegator_by_block_number, test_validator_address, setup_blockchain + 1, endpoint=explorer_endpoint)
assert isinstance(delegations, list)
@pytest.mark.run(order=13)
def test_get_elected_validator_addresses(setup_blockchain):
validator_addresses = _test_staking_rpc(staking.get_elected_validator_addresses)
assert isinstance(validator_addresses, list)
assert len(validator_addresses) > 0
@pytest.mark.run(order=14)
def test_get_validators(setup_blockchain):
validators = _test_staking_rpc(staking.get_validators, 2)
assert isinstance(validators, dict)
assert len(validators['validators']) > 0
@pytest.mark.run(order=15)
def test_get_validator_keys(setup_blockchain):
validators = _test_staking_rpc(staking.get_validator_keys, 2)
assert isinstance(validators, list)
@pytest.mark.run(order=16)
def test_get_validator_self_delegation(setup_blockchain):
self_delegation = _test_staking_rpc(staking.get_validator_self_delegation, test_validator_address)
assert isinstance(self_delegation, int)
assert self_delegation > 0
@pytest.mark.run(order=17)
def test_get_validator_total_delegation(setup_blockchain):
total_delegation = _test_staking_rpc(staking.get_validator_total_delegation, test_validator_address)
assert isinstance(total_delegation, int)
assert total_delegation > 0
@pytest.mark.run(order=18)
def test_get_all_delegation_information(setup_blockchain):
delegation_information = _test_staking_rpc(staking.get_all_delegation_information, 0)
assert isinstance(delegation_information, list)
assert len(delegation_information) > 0
@pytest.mark.run(order=19)
def test_get_delegation_by_delegator_and_validator(setup_blockchain):
delegation_information = _test_staking_rpc(staking.get_delegation_by_delegator_and_validator, test_validator_address, test_validator_address)
assert isinstance(delegation_information, dict)
@pytest.mark.run(order=20)
def test_get_available_redelegation_balance(setup_blockchain):
redelgation_balance = _test_staking_rpc(staking.get_available_redelegation_balance, test_validator_address)
assert isinstance(redelgation_balance, int)
assert redelgation_balance == 0
@pytest.mark.run(order=21)
def test_get_total_staking(setup_blockchain):
total_staking = _test_staking_rpc(staking.get_total_staking)
assert isinstance(total_staking, int)
assert total_staking > 0
@pytest.mark.run(order=22)
def test_errors():
with pytest.raises(exceptions.RPCError):
staking.get_all_validator_addresses(fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_validator_information('', fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_elected_validator_addresses(fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_validators(1, fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_validator_keys(1, fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_validator_information_by_block_number('', 1, fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_all_validator_information(-1, fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_validator_self_delegation('', fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_validator_total_delegation('', fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_all_validator_information_by_block_number(1, 1, fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_all_delegation_information(1, fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_delegations_by_delegator('', fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_delegations_by_delegator_by_block_number('', 1, fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_delegation_by_delegator_and_validator('', '', fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_available_redelegation_balance('', fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_delegations_by_validator('', fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_current_utility_metrics(fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_staking_network_info(fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_super_committees(fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_total_staking(fake_shard)
with pytest.raises(exceptions.RPCError):
staking.get_raw_median_stake_snapshot(fake_shard)

@ -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'

@ -8,6 +8,10 @@ from pyhmy.rpc import (
exceptions
)
from pyhmy.exceptions import (
TxConfirmationTimedoutError
)
localhost_shard_one = 'http://localhost:9501'
tx_hash = '0x1fa20537ea97f162279743139197ecf0eac863278ac1c8ada9a6be5d1e31e633'
@ -18,6 +22,7 @@ stx_hash = '0x57ec011aabdeb078a4816502224022f291fa8b07c82bbae8476f514a1d71c730'
stx_block_num = None
stx_block_hash = None
test_index = 0
fake_shard = 'http://example.com'
# raw_txt generated via:
# hmy transfer --from one12fuf7x9rgtdgqg7vgq0962c556m3p7afsxgvll --to one12fuf7x9rgtdgqg7vgq0962c556m3p7afsxgvll
@ -36,6 +41,8 @@ def _test_transaction_rpc(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)}')
if isinstance(e, TxConfirmationTimedoutError):
pytest.skip(f'{str(e)}')
pytest.fail(f'Unexpected error: {e.__class__} {e}')
return response
@ -52,7 +59,7 @@ def test_get_transaction_by_hash(setup_blockchain):
assert 'blockNumber' in tx.keys()
assert 'blockHash' in tx.keys()
global tx_block_num
tx_block_num = int(tx['blockNumber'], 0)
tx_block_num = int(tx['blockNumber'])
global tx_block_hash
tx_block_hash = tx['blockHash']
@ -86,11 +93,26 @@ def test_get_transaction_error_sink(setup_blockchain):
assert isinstance(errors, list)
@pytest.mark.run(order=7)
def test_send_raw_transaction(setup_blockchain):
def test_send_and_confirm_raw_transaction(setup_blockchain):
# Note: this test is not yet idempotent since the localnet will reject transactions which were previously finalized.
test_tx_hash = _test_transaction_rpc(transaction.send_raw_transaction, raw_tx)
assert isinstance(test_tx_hash, str)
assert test_tx_hash == raw_tx_hash
# Secondly, this is a test that seems to return None values - for example the below curl call has the same null value
# curl --location --request POST 'http://localhost:9501' \
# --header 'Content-Type: application/json' \
# --data-raw '{
# "jsonrpc": "2.0",
# "id": 1,
# "method": "hmyv2_getTransactionByHash",
# "params": [
# "0x86bce2e7765937b776bdcf927339c85421b95c70ddf06ba8e4cc0441142b0f53"
# ]
# }'
# {"jsonrpc":"2.0","id":1,"result":null}
test_tx = _test_transaction_rpc(transaction.send_and_confirm_raw_transaction,
raw_tx) # mining stops by the time this transaction is submitted
# so it never confirms, which is why TxConfirmationTimedoutError
# is in the set up call
assert isinstance(test_tx, dict)
assert test_tx[ 'hash' ] == raw_tx_hash
@pytest.mark.run(order=8)
def test_get_pending_cx_receipts(setup_blockchain):
@ -117,7 +139,7 @@ def test_get_staking_transaction_by_hash(setup_blockchain):
assert 'blockNumber' in staking_tx.keys()
assert 'blockHash' in staking_tx.keys()
global stx_block_num
stx_block_num = int(staking_tx['blockNumber'], 0)
stx_block_num = int(staking_tx['blockNumber'])
global stx_block_hash
stx_block_hash = staking_tx['blockHash']
@ -147,3 +169,50 @@ def test_send_raw_staking_transaction(setup_blockchain):
test_stx_hash = _test_transaction_rpc(transaction.send_raw_staking_transaction, raw_stx, endpoint=localhost_shard_one)
assert isinstance(test_stx_hash, str)
assert test_stx_hash == stx_hash
@pytest.mark.run(order=16)
def test_get_pool_stats(setup_blockchain):
test_pool_stats = _test_transaction_rpc(transaction.get_pool_stats, endpoint=localhost_shard_one)
assert isinstance(test_pool_stats, dict)
@pytest.mark.run(order=17)
def test_get_pending_staking_transactions(setup_blockchain):
pending_staking_transactions = _test_transaction_rpc(transaction.get_pending_staking_transactions, endpoint=localhost_shard_one)
assert isinstance(pending_staking_transactions, list)
@pytest.mark.run(order=18)
def test_errors():
with pytest.raises(exceptions.RPCError):
transaction.get_pending_transactions(fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_transaction_error_sink(fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_pool_stats(fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_transaction_by_hash('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_transaction_by_block_hash_and_index('', 1, endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_transaction_by_block_number_and_index(1, 1, endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_transaction_receipt('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.send_raw_transaction('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_pending_cx_receipts(fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_cx_receipt_by_hash('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.resend_cx_receipt('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_staking_transaction_by_hash('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_staking_transaction_by_block_hash_and_index('', 1, endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_staking_transaction_by_block_number_and_index(1, 1, endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_staking_transaction_error_sink(endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.send_raw_staking_transaction('', endpoint=fake_shard)
with pytest.raises(exceptions.RPCError):
transaction.get_pending_staking_transactions(endpoint=fake_shard)

@ -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()

@ -32,3 +32,23 @@ def test_json_load():
}
loaded_dict = util.json_load(json.dumps(ref_dict))
assert str(ref_dict) == str(loaded_dict)
def test_chain_id_to_int():
assert util.chain_id_to_int(2) == 2
assert util.chain_id_to_int('HmyMainnet') == 1
def test_get_gopath():
assert isinstance(util.get_gopath(), str)
def test_get_goversion():
assert isinstance(util.get_goversion(), str)
def test_convert_one_to_hex():
assert util.convert_one_to_hex('0xebcd16e8c1d8f493ba04e99a56474122d81a9c58') == '0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58'
assert util.convert_one_to_hex('one1a0x3d6xpmr6f8wsyaxd9v36pytvp48zckswvv9') == '0xeBCD16e8c1D8f493bA04E99a56474122D81A9c58'
def test_get_bls_build_variables():
assert isinstance(util.get_bls_build_variables(), dict)
def test_is_active_shard():
assert isinstance(util.is_active_shard(''), bool)

Loading…
Cancel
Save