diff --git a/README.md b/README.md old mode 100755 new mode 100644 index 74d31f1..4f5882f --- a/README.md +++ b/README.md @@ -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() +``` diff --git a/pyhmy/account.py b/pyhmy/account.py index c82c079..d057fe7 100644 --- a/pyhmy/account.py +++ b/pyhmy/account.py @@ -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) diff --git a/pyhmy/blockchain.py b/pyhmy/blockchain.py index d8e318e..50bb3b3 100644 --- a/pyhmy/blockchain.py +++ b/pyhmy/blockchain.py @@ -9,13 +9,13 @@ from .exceptions import ( _default_endpoint = 'http://localhost:9500' _default_timeout = 30 - -################ -# Network RPCs # -################ -def get_node_metadata(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: +############################# +# Node / network level RPCs # +############################# +def get_bad_blocks(endpoint=_default_endpoint, timeout=_default_timeout) -> list: """ - Get config for the node + [WIP] Get list of bad blocks in memory of specific node + Known issues with RPC not returning correctly Parameters ---------- @@ -26,15 +26,26 @@ def get_node_metadata(endpoint=_default_endpoint, timeout=_default_timeout) -> d Returns ------- - dict - # TODO: Add link to reference RPC documentation - """ - return rpc_request('hmy_getNodeMetadata', endpoint=endpoint, timeout=timeout)['result'] + list of bad blocks in node memory + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + API Reference + ------------- + https://api.hmny.io/#0ba3c7b6-6aa9-46b8-9c84-f8782e935951 + """ + method = 'hmyv2_getCurrentBadBlocks' + try: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e -def get_shard(endpoint=_default_endpoint, timeout=_default_timeout) -> int: +def chain_id(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: """ - Get config for the node + Chain id of the chain Parameters ---------- @@ -45,24 +56,27 @@ def get_shard(endpoint=_default_endpoint, timeout=_default_timeout) -> int: Returns ------- - int - Shard ID of node + int that represents the chain id Raises ------ InvalidRPCReplyError If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/343dbe89b3c105f8104ab877769070ba6fdd0133/rpc/blockchain.go#L44 """ - method = 'hmy_getNodeMetadata' + method = 'hmyv2_chainId' try: - return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']['shard-id'] + data = rpc_request(method, endpoint=endpoint, timeout=timeout) + return data['result'] except KeyError as e: raise InvalidRPCReplyError(method, endpoint) from e - -def get_staking_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int: +def get_node_metadata(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: """ - Get epoch number when blockchain switches to EPoS election + Get config for the node Parameters ---------- @@ -73,25 +87,72 @@ def get_staking_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> i Returns ------- - int - Epoch at which blockchain switches to EPoS election + dict with the following keys: + blskey: :obj:`list` of BLS keys on the node + version: :obj:`str` representing the Harmony binary version + network: :obj:`str` the Network name that the node is on (Mainnet or Testnet) + chain-config: :obj:`dict` with the following keys (more are added over time): + chain-id: :obj:`int` Chain ID of the network + cross-tx-epoch: :obj:`int` Epoch at which cross shard transactions were enabled + cross-link-epoch: :obj:`int` Epoch at which cross links were enabled + staking-epoch: :obj:`int` Epoch at which staking was enabled + prestaking-epoch: :obj:`int` Epoch at which staking features without election were allowed + quick-unlock-epoch: :obj:`int` Epoch at which undelegations unlocked in one epoch + eip155-epoch: :obj:`int` Epoch at with EIP155 was enabled + s3-epoch: :obj:`int` Epoch at which Mainnet V0 was launched + receipt-log-epoch: :obj:`int` Epoch at which receipt logs were enabled + eth-compatible-chain-id: :obj:`int` EVM network compatible chain ID + eth-compatible-epoch: :obj:`int` Epoch at which EVM compatibility was launched + eth-compatible-shard-0-chain-id: :obj:`int` EVM network compatible chain ID on shard 0 + five-seconds-epoch: :obj:`int` Epoch at which five second finality was enabled and block rewards adjusted to 17.5 ONE/block + istanbul-epoch: :obj:`int` Epoch at which Ethereum's Istanbul upgrade was added to Harmony + no-early-unlock-epoch: :obj:`int` Epoch at which early unlock of tokens was disabled (https://github.com/harmony-one/harmony/pull/3605) + redelegation-epoch: :obj:`int` Epoch at which redelegation was enabled (staking) + sixty-percent-epoch: :obj:`int` Epoch when internal voting power reduced from 68% to 60% + two-seconds-epoch: :obj:`int` Epoch at which two second finality was enabled and block rewards adjusted to 7 ONE/block + is-leader: :obj:`bool` Whether the node is currently leader or not + shard-id: :obj:`int` Shard that the node is on + current-epoch: :obj:`int` Current epoch + blocks-per-epoch: :obj:`int` Number of blocks per epoch (only available on Shard 0) + role: :obj:`str` Node type(Validator or ExplorerNode) + dns-zone: :obj:`str`: Name of the DNS zone + is-archival: :obj:`bool` Whether the node is currently in state pruning mode or not + node-unix-start-time: :obj:`int` Start time of node un Unix time + p2p-connectivity: :obj:`dict` with the following keys: + connected: :obj:`int` Number of connected peers + not-connected: :obj:`int` Number of peers which are known but not connected + total-known-peers: :obj:`int` Number of peers which are known + peerid: :obj:`str` PeerID, the pubkey for communication + consensus: :obj:`dict` with following keys: + blocknum: :obj:`int` Current block number of the consensus + finality: :obj:`int` The finality time in milliseconds of previous consensus + mode: :obj:`str` Current consensus mode + phase: :obj:`str` Current consensus phase + viewChangeId: :obj:`int` Current view changing ID + viewId: :obj:`int` Current view ID + sync-peers: dictionary of connected sync peers for each shard Raises ------ InvalidRPCReplyError If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#03c39b56-8dfc-48ce-bdad-f85776dd8aec + https://github.com/harmony-one/harmony/blob/v1.10.2/internal/params/config.go#L233 for chain-config dict + https://github.com/harmony-one/harmony/blob/9f320436ff30d9babd957bc5f2e15a1818c86584/node/api.go#L110 for consensus dict """ - method = 'hmy_getNodeMetadata' - data = rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + method = 'hmyv2_getNodeMetadata' try: - return int(data['chain-config']['staking-epoch']) - except (KeyError, TypeError) as e: + metadata = rpc_request(method, endpoint=endpoint, timeout=timeout) + return metadata['result'] + except KeyError as e: raise InvalidRPCReplyError(method, endpoint) from e - -def get_prestaking_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int: +def get_peer_info(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: """ - Get epoch number when blockchain switches to allow staking features without election + Get peer info for the node Parameters ---------- @@ -102,25 +163,33 @@ def get_prestaking_epoch(endpoint=_default_endpoint, timeout=_default_timeout) - Returns ------- - int - Epoch at which blockchain switches to allow staking features without election + if has peers, dict with the following keys: + blocked-peers: :obj:`list` list of blocked peers by peer ID + connected-peers: :obj:`list` list of connected peers by topic + peers: :obj:`list` list of connected peer IDs + topic: :obj:`list` topic of the connection, for example: + 'harmony/0.0.1/client/beacon' + 'harmony/0.0.1/node/beacon' + peerid: :obj:`str` Peer ID of the node Raises ------ InvalidRPCReplyError If received unknown result from endpoint + + See also + -------- + get_node_metadata """ - method = 'hmy_getNodeMetadata' - data = rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + method = 'hmyv2_getPeerInfo' try: - return int(data['chain-config']['prestaking-epoch']) - except (KeyError, TypeError) as e: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: raise InvalidRPCReplyError(method, endpoint) from e - -def get_sharding_structure(endpoint=_default_endpoint, timeout=_default_timeout) -> list: +def protocol_version(endpoint=_default_endpoint, timeout=_default_timeout) -> int: """ - Get network sharding structure + Get the current Harmony protocol version this node supports Parameters ---------- @@ -131,15 +200,28 @@ def get_sharding_structure(endpoint=_default_endpoint, timeout=_default_timeout) Returns ------- - list - # TODO: Add link to reference RPC documentation - """ - return rpc_request('hmy_getShardingStructure', endpoint=endpoint, timeout=timeout)['result'] + int + The current Harmony protocol version this node supports + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + API Reference + ------------- + https://api.hmny.io/#cab9fcc2-e3cd-4bc9-b62a-13e4e046e2fd + """ + method = 'hmyv2_protocolVersion' + try: + value = rpc_request(method, endpoint=endpoint, timeout=timeout) + return value['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e -def get_leader_address(endpoint=_default_endpoint, timeout=_default_timeout) -> str: +def get_num_peers(endpoint=_default_endpoint, timeout=_default_timeout) -> int: """ - Get current leader one address + Get number of peers connected to the node Parameters ---------- @@ -150,15 +232,27 @@ def get_leader_address(endpoint=_default_endpoint, timeout=_default_timeout) -> Returns ------- - str - One address of current leader - """ - return rpc_request('hmy_getLeader', endpoint=endpoint, timeout=timeout)['result'] + int + Number of connected peers + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint -def get_block_number(endpoint=_default_endpoint, timeout=_default_timeout) -> int: + API Reference + ------------- + https://api.hmny.io/#09287e0b-5b61-4d18-a0f1-3afcfc3369c1 """ - Get current block number + method = 'net_peerCount' + try: # Number of peers represented as a hex string + return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16) + except (KeyError, TypeError) as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_version(endpoint=_default_endpoint, timeout=_default_timeout) -> int: + """ + Get version of the EVM network (https://chainid.network/) Parameters ---------- @@ -170,23 +264,26 @@ def get_block_number(endpoint=_default_endpoint, timeout=_default_timeout) -> in Returns ------- int - Current block number + Version if the network Raises ------ InvalidRPCReplyError If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#09287e0b-5b61-4d18-a0f1-3afcfc3369c1 """ - method = 'hmy_blockNumber' + method = 'net_version' try: - return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16) - except TypeError as e: + return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16) # this is hexadecimal + except (KeyError, TypeError) as e: raise InvalidRPCReplyError(method, endpoint) from e - -def get_current_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int: +def in_sync(endpoint=_default_endpoint, timeout=_default_timeout) -> bool: """ - Get current epoch number + Whether the shard chain is in sync or syncing (not out of sync) Parameters ---------- @@ -197,24 +294,26 @@ def get_current_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> i Returns ------- - int - Current epoch number + bool, True if in sync Raises ------ InvalidRPCReplyError If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/blockchain.go#L690 """ - method = 'hmy_getEpoch' + method = 'hmyv2_inSync' try: - return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16) - except TypeError as e: + return bool(rpc_request(method, endpoint=endpoint, timeout=timeout)['result']) + except (KeyError, TypeError) as e: raise InvalidRPCReplyError(method, endpoint) from e - -def get_gas_price(endpoint=_default_endpoint, timeout=_default_timeout) -> int: +def beacon_in_sync(endpoint=_default_endpoint, timeout=_default_timeout) -> bool: """ - Get network gas price + Whether the beacon chain is in sync or syncing (not out of sync) Parameters ---------- @@ -225,24 +324,26 @@ def get_gas_price(endpoint=_default_endpoint, timeout=_default_timeout) -> int: Returns ------- - int - Network gas price + bool, True if sync Raises ------ InvalidRPCReplyError If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/blockchain.go#L695 """ - method = 'hmy_gasPrice' + method = 'hmyv2_beaconInSync' try: - return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16) - except TypeError as e: + return bool(rpc_request(method, endpoint=endpoint, timeout=timeout)['result']) + except (KeyError, TypeError) as e: raise InvalidRPCReplyError(method, endpoint) from e - -def get_num_peers(endpoint=_default_endpoint, timeout=_default_timeout) -> int: +def get_staking_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int: """ - Get number of peers connected to the node + Get epoch number when blockchain switches to EPoS election Parameters ---------- @@ -254,25 +355,31 @@ def get_num_peers(endpoint=_default_endpoint, timeout=_default_timeout) -> int: Returns ------- int - Number of connected peers + Epoch at which blockchain switches to EPoS election + Raises ------ InvalidRPCReplyError If received unknown result from endpoint + + API Reference + --------- + https://github.com/harmony-one/harmony/blob/v1.10.2/internal/params/config.go#L233 + + See also + ------ + get_node_metadata """ - method = 'net_peerCount' + method = 'hmyv2_getNodeMetadata' try: - return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result'], 16) - except TypeError as e: + data = rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + return int(data['chain-config']['staking-epoch']) + except (KeyError, TypeError) as e: raise InvalidRPCReplyError(method, endpoint) from e - -############## -# Block RPCs # -############## -def get_latest_header(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: +def get_prestaking_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int: """ - Get block header of latest block + Get epoch number when blockchain switches to allow staking features without election Parameters ---------- @@ -283,15 +390,35 @@ def get_latest_header(endpoint=_default_endpoint, timeout=_default_timeout) -> d Returns ------- - dict - # TODO: Add link to reference RPC documentation - """ - return rpc_request('hmy_latestHeader', endpoint=endpoint, timeout=timeout)['result'] + int + Epoch at which blockchain switches to allow staking features without election + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/v1.10.2/internal/params/config.go#L233 + See also + ------ + get_node_metadata + """ + method = 'hmyv2_getNodeMetadata' + try: + data = rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + return int(data['chain-config']['prestaking-epoch']) + except (KeyError, TypeError) as e: + raise InvalidRPCReplyError(method, endpoint) from e -def get_latest_headers(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: +######################## +# Sharding information # +######################## +def get_shard(endpoint=_default_endpoint, timeout=_default_timeout) -> int: """ - Get block header of latest block for beacon chain & shard chain + Get shard ID of the node Parameters ---------- @@ -302,168 +429,199 @@ def get_latest_headers(endpoint=_default_endpoint, timeout=_default_timeout) -> Returns ------- - dict - # TODO: Add link to reference RPC documentation - """ - return rpc_request('hmy_getLatestChainHeaders', endpoint=endpoint, timeout=timeout)['result'] + int + Shard ID of node + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint -def get_block_by_number(block_num, endpoint=_default_endpoint, include_full_tx=False, timeout=_default_timeout) -> dict: + See also + -------- + get_node_metadata """ - Get block by number + method = 'hmyv2_getNodeMetadata' + try: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result']['shard-id'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_sharding_structure(endpoint=_default_endpoint, timeout=_default_timeout) -> list: + """ + Get network sharding structure Parameters ---------- - block_num: int - Block number to fetch endpoint: :obj:`str`, optional Endpoint to send request to - include_full_tx: :obj:`bool`, optional - Include list of full transactions data for each block timeout: :obj:`int`, optional Timeout in seconds Returns ------- - dict - # TODO: Add link to reference RPC documentation - """ - params = [ - str(hex(block_num)), - include_full_tx - ] - return rpc_request('hmy_getBlockByNumber', params=params, endpoint=endpoint, timeout=timeout)['result'] + list of dictionaries of shards; each shard has the following keys + shardID: :obj:`int` ID of the shard + current: :obj:`bool` True if the endpoint passed is the same shard as this one + http: :obj:`str` Link to the HTTP(s) API endpoint + wss: :obj:`str` Link to the Web socket endpoint + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + API Reference + ------------- + https://api.hmny.io/#9669d49e-43c1-47d9-a3fd-e7786e5879df + """ + method = 'hmyv2_getShardingStructure' + try: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e -def get_block_by_hash(block_hash, endpoint=_default_endpoint, include_full_tx=False, timeout=_default_timeout) -> dict: +############################# +# Current status of network # +############################# +def get_leader_address(endpoint=_default_endpoint, timeout=_default_timeout) -> str: """ - Get block by hash + Get current leader one address Parameters ---------- - block_hash: str - Block hash to fetch endpoint: :obj:`str`, optional Endpoint to send request to - include_full_tx: :obj:`bool`, optional - Include list of full transactions data for each block timeout: :obj:`int`, optional Timeout in seconds Returns ------- - dict - # TODO: Add link to reference RPC documentation - None if block hash is not found - """ - params = [ - block_hash, - include_full_tx - ] - return rpc_request('hmy_getBlockByHash', params=params, endpoint=endpoint, timeout=timeout)['result'] + str + One address of current leader + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint -def get_block_transaction_count_by_number(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int: + API Reference + ------------- + https://api.hmny.io/#8b08d18c-017b-4b44-a3c3-356f9c12dacd """ - Get transaction count for specific block number + method = 'hmyv2_getLeader' + try: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def is_last_block(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> bool: + """ + If the block at block_num is the last block Parameters ---------- - block_num: int - Block number to get transaction count for + block_num: :obj:`int` + Block number to fetch endpoint: :obj:`str`, optional Endpoint to send request to - include_full_tx: :obj:`bool`, optional - Include list of full transactions data for each block timeout: :obj:`int`, optional Timeout in seconds Returns ------- - int - Number of transactions in the block + bool: True if the block is last epoch block, False otherwise + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/blockchain.go#L286 """ params = [ - str(hex(block_num)) + block_num, ] - return int(rpc_request('hmy_getBlockTransactionCountByNumber', params=params, - endpoint=endpoint, timeout=timeout)['result'], 16 - ) - + method = 'hmyv2_isLastBlock' + try: + return bool(rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result']) + except (KeyError, TypeError) as e: + raise InvalidRPCReplyError(method, endpoint) from e -def get_block_transaction_count_by_hash(block_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> int: +def epoch_last_block(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> int: """ - Get transaction count for specific block hash for + Returns the number of the last block in the epoch Parameters ---------- - block_hash: str - Block hash to get transaction count + epoch: :obj:`int` + Epoch for which the last block is to be fetched endpoint: :obj:`str`, optional Endpoint to send request to - include_full_tx: :obj:`bool`, optional - Include list of full transactions data for each block timeout: :obj:`int`, optional Timeout in seconds Returns ------- - int - Number of transactions in the block + int: Number of the last block in the epoch + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/blockchain.go#L294 """ params = [ - block_hash + epoch, ] - return int(rpc_request('hmy_getBlockTransactionCountByHash', params=params, - endpoint=endpoint, timeout=timeout)['result'], 16 - ) - + method = 'hmyv2_epochLastBlock' + 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_blocks(start_block, end_block, endpoint=_default_endpoint, include_full_tx=False, - include_signers=False, timeout=_default_timeout - ) -> list: +def get_circulating_supply(endpoint=_default_endpoint, timeout=_default_timeout) -> int: """ - Get list of blocks from a range + Get current circulation supply of tokens in ONE Parameters ---------- - start_block: int - First block to fetch (inclusive) - end_block: int - Last block to fetch (inclusive) endpoint: :obj:`str`, optional Endpoint to send request to - include_full_tx: :obj:`bool`, optional - Include list of full transactions data for each block - include_signers: :obj:`bool`, optional - Include list of signers for each block timeout: :obj:`int`, optional Timeout in seconds Returns ------- - list - # TODO: Add link to reference RPC documentation - """ - params = [ - str(hex(start_block)), - str(hex(end_block)), - { - 'withSigners': include_signers, - 'fullTx': include_full_tx, - }, - ] - return rpc_request('hmy_getBlocks', params=params, endpoint=endpoint, timeout=timeout)['result'] + str + Current circulation supply (with decimal point) + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + API Reference + ------------- + https://api.hmny.io/#8398e818-ac2d-4ad8-a3b4-a00927395044 + """ + method = 'hmyv2_getCirculatingSupply' + try: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e -def get_block_signers(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list: +def get_total_supply(endpoint=_default_endpoint, timeout=_default_timeout) -> int: """ - Get list of block signers for specific block number + Get total number of pre-mined tokens Parameters ---------- - block_num: int - Block number to get signers for endpoint: :obj:`str`, optional Endpoint to send request to timeout: :obj:`int`, optional @@ -471,22 +629,665 @@ def get_block_signers(block_num, endpoint=_default_endpoint, timeout=_default_ti Returns ------- - list - List of one addresses that signed the block - """ - params = [ - str(hex(block_num)) - ] - return rpc_request('hmy_getBlockSigners', params=params, endpoint=endpoint, timeout=timeout)['result'] + str + Total number of pre-mined tokens, or None if no such tokens + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#3dcea518-9e9a-4a20-84f4-c7a0817b2196 + """ + method = 'hmyv2_getTotalSupply' + try: + rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_block_number(endpoint=_default_endpoint, timeout=_default_timeout) -> int: + """ + Get current block number + + Parameters + ---------- + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + int + Current block number + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#2602b6c4-a579-4b7c-bce8-85331e0db1a7 + """ + method = 'hmyv2_blockNumber' + try: + return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result']) + except (KeyError, TypeError) as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_current_epoch(endpoint=_default_endpoint, timeout=_default_timeout) -> int: + """ + Get current epoch number + + Parameters + ---------- + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + int + Current epoch number + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#9b8e98b0-46d1-4fa0-aaa6-317ff1ddba59 + """ + method = 'hmyv2_getEpoch' + try: + return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result']) + except (KeyError, TypeError) as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_last_cross_links(endpoint=_default_endpoint, timeout=_default_timeout) -> list: + """ + Get last cross shard links + + Parameters + ---------- + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + list of dictionaries, one for each shard except the one at the endpoint; each representing + the last block on the beacon-chain + hash: :obj:`str` Parent block hash + block-number: :obj:`int` Block number + view-id: :obj:`int` View ID + signature: :obj:`str` Hex representation of aggregated signature + signature-bitmap: :obj:`str` Hex representation of aggregated signature bitmap + shard-id: :obj:`str` (other) shard ID + epoch-number: :obj:`int` Block epoch + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#4994cdf9-38c4-4b1d-90a8-290ddaa3040e + """ + method = 'hmyv2_getLastCrossLinks' + try: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_gas_price(endpoint=_default_endpoint, timeout=_default_timeout) -> int: + """ + Get network gas price + + Parameters + ---------- + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + int + Network gas price + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#1d53fd59-a89f-436c-a171-aec9d9623f48 + """ + method = 'hmyv2_gasPrice' + try: + return int(rpc_request(method, endpoint=endpoint, timeout=timeout)['result']) + except (KeyError, TypeError) as e: + raise InvalidRPCReplyError(method, endpoint) from e + +############## +# Block RPCs # +############## +def get_latest_header(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: + """ + Get block header of latest block + + Parameters + ---------- + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + dict with the following keys: + blockHash: :obj:`str` Block hash + blockNumber: :obj:`int` Block number + shardID: :obj:`int` Shard ID + leader: :obj:`str` Wallet address of leader that proposed this block if prestaking, otherwise sha256 hash of leader's public bls key + viewID: :obj:`int` View ID of the block + epoch: :obj:`int` Epoch of block + timestamp: :obj:`str` Timestamp that the block was finalized in human readable format + unixtime: :obj:`int` Timestamp that the block was finalized in Unix time + lastCommitSig: :obj:`str` Hex representation of aggregated signatures of the previous block + lastCommitBitmap: :obj:`str` Hex representation of aggregated signature bitmap of the previous block + crossLinks: list of dicts describing the cross shard links, each dict to have the following keys: + block-number: :obj:`int` Number of the cross link block + epoch-number: :obj:`int` Epoch of the cross link block + hash: :obj:`str` Hash of the cross link block + shard-id: :obj:`int` Shard ID for the cross link (besides the shard at endpoint) + signature: :obj:`str` Aggregated signature of the cross link block + siganture-bitmap: :obj:`str` Aggregated signature bitmap of the cross link block + view-id: :obj:`int` View ID of the cross link block + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#73fc9b97-b048-4b85-8a93-4d2bf1da54a6 + """ + method = 'hmyv2_latestHeader' + try: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_header_by_number(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> dict: + """ + Get block header of block at block_num + + Parameters + ---------- + block_num: :obj:`int` + Number of the block whose header is requested + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + See get_latest_header for header structure + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#01148e4f-72bb-426d-a123-718a161eaec0 + """ + method = 'hmyv2_getHeaderByNumber' + params = [ + block_num + ] + try: + return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_latest_chain_headers(endpoint=_default_endpoint, timeout=_default_timeout) -> dict: + """ + Get block header of latest block for beacon chain & shard chain + + Parameters + ---------- + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + dict with two keys: + beacon-chain-header: :obj:`dict` with the following keys, applicable to the beacon chain (cross shard links) + shard-chain-header: :obj:`dict` with the following keys, applicable to the shard chain + difficulty: legacy + epoch: :obj:`int` Epoch of the block + extraData: legacy + gasLimit: legacy + gasUsed: legacy + hash: :obj:`int` Hash of the block + logsBloom: legacy + miner: legacy + mixHash: legacy + nonce: legacy + number: :obj:`int` Block number + parentHash: legacy + receiptsRoot: legacy + sha3Uncles: legacy + shardID :obj:`int` Shard ID + stateRoot: legacy + timestamp: legacy + transactionsRoot: legacy + viewID: View ID + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#7625493d-16bf-4611-8009-9635d063b4c0 + """ + method = 'hmyv2_getLatestChainHeaders' + try: + return rpc_request(method, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_block_by_number(block_num, full_tx=False, include_tx=False, include_staking_tx=False, + include_signers=False, endpoint=_default_endpoint, timeout=_default_timeout) -> dict: + """ + Get block by number + + Parameters + ---------- + block_num: :obj:`int` + Block number to fetch + full_tx: :obj:`bool`, optional + Include full transactions data for the block + include_tx: :obj:`bool`, optional + Include regular transactions for the block + include_staking_tx: :obj:`bool`, optional + Include staking transactions for the block + include_signers: :obj:`bool`, optional + Include list of signers for the block + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + dict with the following keys + difficulty: legacy + epoch: :obj:`int` Epoch number of block + extraData: :obj:`str` Hex representation of extra data in the block + gasLimit: :obj:`int` Maximum gas that can be used for transactions in the block + gasUsed: :obj:`int` Gas that was actually used for transactions in the block + hash: :obj:`str` Block hash + logsBloom: :obj:`str` Bloom logs + miner: :obj:`str` Wallet address of the leader that proposed this block + mixHash: legacy + nonce: legacy + number: :obj:`int` Block number + parentHash: :obj:`str` Hash of parent block + receiptsRoot: :obj:`str` Hash of transaction receipt root + signers: :obj:`list` List of signers (only if include_signers is set to True) + size: :obj:`int` Block size in bytes + stakingTransactions: :obj:`list` + if full_tx is True: List of dictionaries, each containing a staking transaction (see account.get_staking_transaction_history) + if full_tx is False: List of staking transaction hashes + stateRoot: :obj:`str` Hash of state root + timestamp: :obj:`int` Unix timestamp of the block + transactions: :obj:`list` + if full_tx is True: List of dictionaries, each containing a transaction (see account.get_transaction_history) + if full_tx is False: List of transaction hashes + transactionsRoot: :obj:`str` Hash of transactions root + uncles: :obj:`str` legacy + viewID: :obj:`int` View ID + transactionsInEthHash: :obj:`str` Transactions in ethereum hash + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#52f8a4ce-d357-46f1-83fd-d100989a8243 + """ + params = [ + block_num, + { + 'inclTx': include_tx, + 'fullTx': full_tx, + 'inclStaking': include_staking_tx, + 'withSigners': include_signers, + }, + ] + method = 'hmyv2_getBlockByNumber' + try: + return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_block_by_hash(block_hash, full_tx=False, include_tx=False, include_staking_tx=False, + include_signers=False, endpoint=_default_endpoint, timeout=_default_timeout) -> dict: + """ + Get block by hash + + Parameters + ---------- + block_hash: :obj:`str` + Block hash to fetch + full_tx: :obj:`bool`, optional + Include full transactions data for the block + include_tx: :obj:`bool`, optional + Include regular transactions for the block + include_staking_tx: :obj:`bool`, optional + Include staking transactions for the block + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + See get_block_by_number for block structure + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#6a49ec47-1f74-4732-9f04-e5d76160bd5c + """ + params = [ + block_hash, + { + 'inclTx': include_tx, + 'fullTx': full_tx, + 'inclStaking': include_staking_tx, + 'withSigners': include_signers, + }, + ] + method = 'hmyv2_getBlockByHash' + try: + return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_block_transaction_count_by_number(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int: + """ + Get transaction count for specific block number + + Parameters + ---------- + block_num: :obj:`int` + Block number to get transaction count for + endpoint: :obj:`str`, optional + Endpoint to send request to + include_full_tx: :obj:`bool`, optional + Include list of full transactions data for each block + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + int + Number of transactions in the block + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#26c5adfb-d757-4595-9eb7-c6efef63df32 + """ + params = [ + block_num + ] + method = 'hmyv2_getBlockTransactionCountByNumber' + 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_block_transaction_count_by_hash(block_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> int: + """ + Get transaction count for specific block hash + + Parameters + ---------- + block_hash: :obj:`str` + Block hash to get transaction count + endpoint: :obj:`str`, optional + Endpoint to send request to + include_full_tx: :obj:`bool`, optional + Include list of full transactions data for each block + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + int + Number of transactions in the block + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#66c68844-0208-49bb-a83b-08722bc113eb + """ + params = [ + block_hash + ] + method = 'hmyv2_getBlockTransactionCountByHash' + 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_block_staking_transaction_count_by_number(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> int: + """ + Get staking transaction count for specific block number + + Parameters + ---------- + block_num: :obj:`int` + Block number to get transaction count for + endpoint: :obj:`str`, optional + Endpoint to send request to + include_full_tx: :obj:`bool`, optional + Include list of full transactions data for each block + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + int + Number of staking transactions in the block + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/transaction.go#L494 + """ + params = [ + block_num + ] + method = 'hmyv2_getBlockStakingTransactionCountByNumber' + 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_block_staking_transaction_count_by_hash(block_hash, endpoint=_default_endpoint, timeout=_default_timeout) -> int: + """ + Get staking transaction count for specific block hash + + Parameters + ---------- + block_hash: :obj:`str` + Block hash to get transaction count + endpoint: :obj:`str`, optional + Endpoint to send request to + include_full_tx: :obj:`bool`, optional + Include list of full transactions data for each block + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + int + Number of transactions in the block + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/transaction.go#L523 + """ + params = [ + block_hash + ] + method = 'hmyv2_getBlockStakingTransactionCountByHash' + 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_blocks(start_block, end_block, full_tx=False, include_tx=False, include_staking_tx=False, + include_signers=False, endpoint=_default_endpoint, timeout=_default_timeout + ) -> list: + """ + Get list of blocks from a range + + Parameters + ---------- + start_block: :obj:`int` + First block to fetch (inclusive) + end_block: :obj:`int` + Last block to fetch (inclusive) + full_tx: :obj:`bool`, optional + Include full transactions data for the block + include_tx: :obj:`bool`, optional + Include regular transactions for the block + include_staking_tx: :obj:`bool`, optional + Include staking transactions for the block + include_signers: :obj:`bool`, optional + Include list of signers for the block + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + list of blocks, see get_block_by_number for block structure + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#ab9bdc59-e482-436c-ab2f-10df215cd0bd + """ + params = [ + start_block, + end_block, + { + 'withSigners': include_signers, + 'fullTx': full_tx, + 'inclStaking': include_staking_tx, + 'inclTx': include_tx + }, + ] + method = 'hmyv2_getBlocks' + try: + return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e -def get_block_signer_keys(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list: +def get_block_signers(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list: + """ + Get list of block signers for specific block number + + Parameters + ---------- + block_num: :obj:`int` + Block number to get signers for + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + list + List of one addresses that signed the block + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#1e4b5f41-9db6-4dea-92fb-4408db78e622 + """ + params = [ + block_num + ] + method = 'hmyv2_getBlockSigners' + try: + return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e + +def get_block_signers_keys(block_num, endpoint=_default_endpoint, timeout=_default_timeout) -> list: """ Get list of block signer public bls keys for specific block number Parameters ---------- - block_num: int + block_num: :obj:`int` Block number to get signer keys for endpoint: :obj:`str`, optional Endpoint to send request to @@ -497,21 +1298,35 @@ def get_block_signer_keys(block_num, endpoint=_default_endpoint, timeout=_defaul ------- list List of bls public keys that signed the block + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#9f9c8298-1a4e-4901-beac-f34b59ed02f1 """ params = [ - str(hex(block_num)) + block_num ] - return rpc_request('hmy_getBlockSignerKeys', params=params, endpoint=endpoint, timeout=timeout)['result'] - + method = 'hmyv2_getBlockSignerKeys' + try: + return rpc_request(method, params=params, 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) -> dict: +def is_block_signer(block_num, address, endpoint=_default_endpoint, timeout=_default_timeout) -> bool: """ - Get list of validators for specific epoch number + Determine if the account at address is a signer for the block at block_num Parameters ---------- - epoch: int - Epoch to get list of validators for + block_num: :obj:`int` + Block number to check + address: :obj:`str` + Address to check endpoint: :obj:`str`, optional Endpoint to send request to timeout: :obj:`int`, optional @@ -519,23 +1334,70 @@ def get_validators(epoch, endpoint=_default_endpoint, timeout=_default_timeout) Returns ------- - dict - # TODO: Add link to reference RPC documentation + bool: True if the address was a signer for block_num, False otherwise + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/blockchain.go#L368 """ params = [ - epoch + block_num, + address ] - return rpc_request('hmy_getValidators', params=params, endpoint=endpoint, timeout=timeout)['result'] + method = 'hmyv2_isBlockSigner' + try: + return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e +def get_signed_blocks(address, endpoint=_default_endpoint, timeout=_default_timeout) -> bool: + """ + The number of blocks a particular validator signed for last blocksPeriod (1 epoch) -def get_validator_keys(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> list: + Parameters + ---------- + address: :obj:`str` + Address to check + endpoint: :obj:`str`, optional + Endpoint to send request to + timeout: :obj:`int`, optional + Timeout in seconds + + Returns + ------- + int: Number of blocks signed by account at address for last blocksPeriod + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://github.com/harmony-one/harmony/blob/1a8494c069dc3f708fdf690456713a2411465199/rpc/blockchain.go#L406 """ - Get list of validator public bls keys for specific epoch number + params = [ + address + ] + method = 'hmyv2_getSignedBlocks' + 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_validators(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> dict: + """ + Get list of validators for specific epoch number Parameters ---------- - epoch: int - Epoch to get list of validator keys for + epoch: :obj:`int` + Epoch to get list of validators for endpoint: :obj:`str`, optional Endpoint to send request to timeout: :obj:`int`, optional @@ -543,21 +1405,38 @@ def get_validator_keys(epoch, endpoint=_default_endpoint, timeout=_default_timeo Returns ------- - list - List of bls public keys in the validator committee + dict with the following keys + shardID: :obj:`int` ID of the shard + validators: :obj:`list` of dictionaries, each with the following keys + address: :obj:`str` address of the validator + balance: :obj:`int` balance of the validator in ATTO + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#4dfe91ad-71fa-4c7d-83f3-d1c86a804da5 """ params = [ epoch ] - return rpc_request('hmy_getValidatorKeys', params=params, endpoint=endpoint, timeout=timeout)['result'] - + method = 'hmyv2_getValidators' + try: + return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e -def get_bad_blocks(endpoint=_default_endpoint, timeout=_default_timeout) -> list: +def get_validator_keys(epoch, endpoint=_default_endpoint, timeout=_default_timeout) -> list: """ - Get list of bad blocks in memory of specific node + Get list of validator public bls keys for specific epoch number Parameters ---------- + epoch: :obj:`int` + Epoch to get list of validator keys for endpoint: :obj:`str`, optional Endpoint to send request to timeout: :obj:`int`, optional @@ -566,6 +1445,22 @@ def get_bad_blocks(endpoint=_default_endpoint, timeout=_default_timeout) -> list Returns ------- list - # TODO: Add link to reference RPC documentation + List of bls public keys in the validator committee + + Raises + ------ + InvalidRPCReplyError + If received unknown result from endpoint + + API Reference + ------------- + https://api.hmny.io/#1439b580-fa3c-4d44-a79d-303390997a8c """ - return rpc_request('hmy_getCurrentBadBlocks', endpoint=endpoint, timeout=timeout)['result'] + params = [ + epoch + ] + method = 'hmyv2_getValidatorKeys' + try: + return rpc_request(method, params=params, endpoint=endpoint, timeout=timeout)['result'] + except KeyError as e: + raise InvalidRPCReplyError(method, endpoint) from e diff --git a/pyhmy/contract.py b/pyhmy/contract.py new file mode 100644 index 0000000..90b41df --- /dev/null +++ b/pyhmy/contract.py @@ -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 diff --git a/pyhmy/exceptions.py b/pyhmy/exceptions.py index ba1da9b..2238169 100644 --- a/pyhmy/exceptions.py +++ b/pyhmy/exceptions.py @@ -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}') diff --git a/pyhmy/signing.py b/pyhmy/signing.py new file mode 100644 index 0000000..c773936 --- /dev/null +++ b/pyhmy/signing.py @@ -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, + ) diff --git a/pyhmy/staking.py b/pyhmy/staking.py index 026580c..4186073 100644 --- a/pyhmy/staking.py +++ b/pyhmy/staking.py @@ -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 diff --git a/pyhmy/staking_signing.py b/pyhmy/staking_signing.py new file mode 100644 index 0000000..d1cd69a --- /dev/null +++ b/pyhmy/staking_signing.py @@ -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) diff --git a/pyhmy/staking_structures.py b/pyhmy/staking_structures.py new file mode 100644 index 0000000..de64ed7 --- /dev/null +++ b/pyhmy/staking_structures.py @@ -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 diff --git a/pyhmy/transaction.py b/pyhmy/transaction.py index e897764..930adf6 100644 --- a/pyhmy/transaction.py +++ b/pyhmy/transaction.py @@ -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 diff --git a/pyhmy/util.py b/pyhmy/util.py index e9c4960..6ef6fc5 100644 --- a/pyhmy/util.py +++ b/pyhmy/util.py @@ -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): """ diff --git a/pyhmy/validator.py b/pyhmy/validator.py index 2c3b832..111644a 100644 --- a/pyhmy/validator.py +++ b/pyhmy/validator.py @@ -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) diff --git a/setup.py b/setup.py index eb0ee53..5649319 100644 --- a/setup.py +++ b/setup.py @@ -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', diff --git a/tests/bech32-pyhmy/test_bech32.py b/tests/bech32-pyhmy/test_bech32.py new file mode 100644 index 0000000..325bc08 --- /dev/null +++ b/tests/bech32-pyhmy/test_bech32.py @@ -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') diff --git a/tests/request-pyhmy/test_request.py b/tests/request-pyhmy/test_request.py index 6315014..a7fcc97 100644 --- a/tests/request-pyhmy/test_request.py +++ b/tests/request-pyhmy/test_request.py @@ -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", diff --git a/tests/sdk-pyhmy/conftest.py b/tests/sdk-pyhmy/conftest.py index 07b037c..ef421ec 100644 --- a/tests/sdk-pyhmy/conftest.py +++ b/tests/sdk-pyhmy/conftest.py @@ -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) diff --git a/tests/sdk-pyhmy/test_account.py b/tests/sdk-pyhmy/test_account.py index b2f1564..8bf0706 100644 --- a/tests/sdk-pyhmy/test_account.py +++ b/tests/sdk-pyhmy/test_account.py @@ -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) diff --git a/tests/sdk-pyhmy/test_blockchain.py b/tests/sdk-pyhmy/test_blockchain.py index 034bf6b..edbfb10 100644 --- a/tests/sdk-pyhmy/test_blockchain.py +++ b/tests/sdk-pyhmy/test_blockchain.py @@ -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) diff --git a/tests/sdk-pyhmy/test_contract.py b/tests/sdk-pyhmy/test_contract.py new file mode 100644 index 0000000..a5cbea8 --- /dev/null +++ b/tests/sdk-pyhmy/test_contract.py @@ -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) diff --git a/tests/sdk-pyhmy/test_signing.py b/tests/sdk-pyhmy/test_signing.py new file mode 100644 index 0000000..34ac601 --- /dev/null +++ b/tests/sdk-pyhmy/test_signing.py @@ -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' diff --git a/tests/sdk-pyhmy/test_staking.py b/tests/sdk-pyhmy/test_staking.py index 934d979..115f834 100644 --- a/tests/sdk-pyhmy/test_staking.py +++ b/tests/sdk-pyhmy/test_staking.py @@ -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) diff --git a/tests/sdk-pyhmy/test_staking_signing.py b/tests/sdk-pyhmy/test_staking_signing.py new file mode 100644 index 0000000..2b15c3d --- /dev/null +++ b/tests/sdk-pyhmy/test_staking_signing.py @@ -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' diff --git a/tests/sdk-pyhmy/test_transaction.py b/tests/sdk-pyhmy/test_transaction.py index 9cc3503..fde7c1b 100644 --- a/tests/sdk-pyhmy/test_transaction.py +++ b/tests/sdk-pyhmy/test_transaction.py @@ -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) diff --git a/tests/sdk-pyhmy/test_validator.py b/tests/sdk-pyhmy/test_validator.py new file mode 100644 index 0000000..34bc436 --- /dev/null +++ b/tests/sdk-pyhmy/test_validator.py @@ -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() diff --git a/tests/util-pyhmy/test_util.py b/tests/util-pyhmy/test_util.py index 1595022..d502d11 100644 --- a/tests/util-pyhmy/test_util.py +++ b/tests/util-pyhmy/test_util.py @@ -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)