diff --git a/.gitignore b/.gitignore index ee06ff85..2f359c4d 100644 --- a/.gitignore +++ b/.gitignore @@ -5,7 +5,6 @@ __pycache__ *.py[cod] *$py.class .pytest_cache/ -erdpy/tests/testdata-out .idea diff --git a/erdpy/CHANGELOG.md b/erdpy/CHANGELOG.md index 968f54ee..9d2cf064 100644 --- a/erdpy/CHANGELOG.md +++ b/erdpy/CHANGELOG.md @@ -7,6 +7,12 @@ Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how ## [Unreleased] - TBD +## [3.0.3] + - Fixes after repository migrations + +## [3.0.2] + - Fixes after repository migrations + ## [3.0.1] - [Contract verify: fix CLI arguments & json request](https://github.com/ElrondNetwork/elrond-sdk-erdpy/pull/171) diff --git a/erdpy/_version.py b/erdpy/_version.py index 05527687..8d1c8625 100644 --- a/erdpy/_version.py +++ b/erdpy/_version.py @@ -1 +1 @@ -__version__ = "3.0.1" +__version__ = "3.0.3" diff --git a/erdpy/accounts.py b/erdpy/accounts.py index e3d4ce5b..e7293501 100644 --- a/erdpy/accounts.py +++ b/erdpy/accounts.py @@ -1,11 +1,12 @@ import logging from pathlib import Path -from typing import Any, Optional +from typing import Any, Optional, Protocol import nacl.signing from erdpy import constants, errors from erdpy.interfaces import IAccount, IAddress, ITransaction +from erdpy_network_providers.accounts import AccountOnNetwork from erdpy.ledger.config import compare_versions from erdpy.ledger.ledger_app_handler import SIGN_USING_HASH_VERSION from erdpy.ledger.ledger_functions import do_get_ledger_address, do_sign_transaction_with_ledger, do_get_ledger_version, \ @@ -16,6 +17,11 @@ logger = logging.getLogger("accounts") +class INetworkProvider(Protocol): + def get_account(self, address: IAddress) -> AccountOnNetwork: + ... + + class Account(IAccount): def __init__(self, address: Any = None, @@ -39,9 +45,9 @@ def __init__(self, self.secret_key = secret_key.hex() self.address = Address(address_from_key_file) - def sync_nonce(self, proxy: Any): + def sync_nonce(self, proxy: INetworkProvider): logger.info("Account.sync_nonce()") - self.nonce = proxy.get_account_nonce(self.address) + self.nonce = proxy.get_account(self.address).nonce logger.info(f"Account.sync_nonce() done: {self.nonce}") def sign_transaction(self, transaction: ITransaction) -> str: diff --git a/erdpy/accounts_repository.py b/erdpy/accounts_repository.py index f9e106fc..df336e90 100644 --- a/erdpy/accounts_repository.py +++ b/erdpy/accounts_repository.py @@ -1,16 +1,22 @@ import logging from multiprocessing.pool import ThreadPool from pathlib import Path -from typing import List, Set, Union +from typing import List, Set, Union, Protocol from erdpy import utils from erdpy.accounts import Account, Address -from erdpy.interfaces import IElrondProxy +from erdpy_network_providers.accounts import AccountOnNetwork +from erdpy.interfaces import IAddress from erdpy.wallet import pem logger = logging.getLogger("accounts") +class INetworkProvider(Protocol): + def get_account(self, address: IAddress) -> AccountOnNetwork: + ... + + class AccountsRepository: def __init__(self, accounts: List[Account]): self.accounts = accounts @@ -66,7 +72,7 @@ def get_all(self) -> List[Account]: def __len__(self): return len(self.accounts) - def sync_nonces(self, proxy: IElrondProxy, num_parallel: int = 10): + def sync_nonces(self, proxy: INetworkProvider, num_parallel: int = 10): logger.info("Sync nonces for", len(self.accounts), "accounts") def sync_nonce(account: Account): diff --git a/erdpy/cli.py b/erdpy/cli.py index 23d1b369..c451695e 100644 --- a/erdpy/cli.py +++ b/erdpy/cli.py @@ -24,9 +24,9 @@ logger = logging.getLogger("cli") -def main(): +def main(cli_args: List[str] = []): try: - _do_main() + _do_main(cli_args) except errors.KnownError as err: logger.critical(err.get_pretty()) return 1 @@ -36,11 +36,11 @@ def main(): return 0 -def _do_main(): +def _do_main(cli_args: List[str]): logging.basicConfig(level=logging.INFO) scope.initialize() - argv_with_config_args = config.add_config_args(sys.argv[1:]) + argv_with_config_args = config.add_config_args(cli_args) parser = setup_parser(argv_with_config_args) args = parser.parse_args(argv_with_config_args) @@ -55,7 +55,7 @@ def _do_main(): args.func(args) -def setup_parser(args: List[str] = sys.argv[1:]): +def setup_parser(args: List[str]): parser = ArgumentParser( prog="erdpy", usage="erdpy [-h] [-v] [--verbose] COMMAND-GROUP [-h] COMMAND ...", @@ -108,5 +108,5 @@ def setup_parser(args: List[str] = sys.argv[1:]): if __name__ == "__main__": - ret = main() + ret = main(sys.argv[1:]) sys.exit(ret) diff --git a/erdpy/cli_accounts.py b/erdpy/cli_accounts.py index 28b4ea50..6dfc2f75 100644 --- a/erdpy/cli_accounts.py +++ b/erdpy/cli_accounts.py @@ -3,7 +3,7 @@ from erdpy import cli_shared, utils from erdpy.accounts import Address -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider logger = logging.getLogger("cli.accounts") @@ -39,25 +39,24 @@ def _add_address_arg(sub: Any): def get_account(args: Any): proxy_url = args.proxy address = args.address - proxy = ElrondProxy(proxy_url) + proxy = ProxyNetworkProvider(proxy_url) account = proxy.get_account(Address(address)) - omit_fields = cli_shared.parse_omit_fields_arg(args) if args.balance: - print(account.get("balance", 0)) + print(account.balance) elif args.nonce: - print(account.get("nonce", 0)) + print(account.nonce) elif args.username: - print(account.get("username", 0)) + print(account.username) else: - utils.omit_fields(account, omit_fields) - utils.dump_out_json(account) + utils.dump_out_json(account.to_dictionary()) def get_account_transactions(args: Any): proxy_url = args.proxy address = args.address - proxy = ElrondProxy(proxy_url) + proxy = ProxyNetworkProvider(proxy_url) response = proxy.get_account_transactions(Address(address)) - utils.dump_out_json(response, args.outfile) + transactions_as_dictionaries = [tx.to_dictionary() for tx in response] + utils.dump_out_json(transactions_as_dictionaries, args.outfile) diff --git a/erdpy/cli_block.py b/erdpy/cli_block.py index d1bc42fb..6e0ef57e 100644 --- a/erdpy/cli_block.py +++ b/erdpy/cli_block.py @@ -1,7 +1,7 @@ from typing import Any from erdpy import cli_shared, utils -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider def setup_parser(subparsers: Any) -> Any: @@ -18,6 +18,14 @@ def setup_parser(subparsers: Any) -> Any: def get_hyperblock(args: Any) -> Any: proxy_url = args.proxy - proxy = ElrondProxy(proxy_url) - response = proxy.get_hyperblock(args.key) + proxy = ProxyNetworkProvider(proxy_url) + key = args.key + + url = f"hyperblock/by-hash/{key}" + if str(key).isnumeric(): + url = f"hyperblock/by-nonce/{key}" + + response = proxy.do_get_generic(url) + response = response.get("hyperblock", {}) + utils.dump_out_json(response) diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py index 091be35b..8ba7e729 100644 --- a/erdpy/cli_contracts.py +++ b/erdpy/cli_contracts.py @@ -11,7 +11,7 @@ from erdpy.contracts import CodeMetadata, SmartContract from erdpy.projects import load_project from erdpy.projects.core import get_project_paths_recursively -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider from erdpy.transactions import Transaction from erdpy.docker import is_docker_installed, run_docker from erdpy.errors import DockerMissingError @@ -337,7 +337,7 @@ def _prepare_sender(args: Any) -> Account: sender.nonce = args.nonce if args.recall_nonce: - sender.sync_nonce(ElrondProxy(args.proxy)) + sender.sync_nonce(ProxyNetworkProvider(args.proxy)) return sender @@ -408,7 +408,7 @@ def query(args: Any): arguments = args.arguments contract = SmartContract(contract_address) - result = contract.query(ElrondProxy(args.proxy), function, arguments) + result = contract.query(ProxyNetworkProvider(args.proxy), function, arguments) utils.dump_out_json(result) diff --git a/erdpy/cli_delegation.py b/erdpy/cli_delegation.py index b7d64cf5..2a8488c1 100644 --- a/erdpy/cli_delegation.py +++ b/erdpy/cli_delegation.py @@ -1,10 +1,8 @@ -import binascii from typing import Any, List from erdpy import cli_shared, errors, utils -from erdpy.accounts import Address from erdpy.delegation import staking_provider -from erdpy.proxy import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider from erdpy.transactions import do_prepare_transaction @@ -27,7 +25,6 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.add_argument("--create-tx-hash", required=True, help="the hash") sub.add_argument("--sender", required=False, help="the sender address") cli_shared.add_proxy_arg(sub) - cli_shared.add_omit_fields_arg(sub) sub.set_defaults(func=get_contract_address_by_deploy_tx_hash) # add a new node @@ -150,13 +147,16 @@ def do_create_delegation_contract(args: Any): def get_contract_address_by_deploy_tx_hash(args: Any): args = utils.as_object(args) - omit_fields = cli_shared.parse_omit_fields_arg(args) - proxy = ElrondProxy(args.proxy) + proxy = ProxyNetworkProvider(args.proxy) - transaction = proxy.get_transaction(args.create_tx_hash, with_results=True) - utils.omit_fields(transaction, omit_fields) - _get_sc_address_from_tx(transaction) + transaction = proxy.get_transaction(args.create_tx_hash) + transaction_events = transaction.logs.events + if len(transaction_events) == 1: + contract_address = transaction_events[0].address + print(contract_address.bech32()) + else: + raise errors.ProgrammingError("Tx has more than one event. Make sure it's a staking provider SC Deploy transaction.") def add_new_nodes(args: Any): @@ -255,29 +255,3 @@ def set_metadata(args: Any): tx = do_prepare_transaction(args) cli_shared.send_or_simulate(tx, args) - - -def _get_sc_address_from_tx(data: Any): - if not isinstance(data, dict): - raise errors.ProgrammingError("error") - - sc_results = data.get('smartContractResults') - if sc_results is None: - raise errors.ProgrammingError("smart contract results missing") - - # TODO improve robustness of this code in case of failed transaction - try: - sc_result = sc_results[0] - data_field = sc_result['data'] - - data_field_split = data_field.split('@') - arg_1 = binascii.unhexlify(data_field_split[1]) - if not arg_1 == b'ok': - raise errors.ProgrammingError(arg_1.decode("utf-8")) - - sc_address = binascii.unhexlify(data_field_split[2]) - address = Address(sc_address) - print("Contract address: ", address) - except Exception: - raise errors.ProgrammingError( - "cannot get the smart contract address from transaction results, please try again") diff --git a/erdpy/cli_dns.py b/erdpy/cli_dns.py index 69090a01..6bec3f09 100644 --- a/erdpy/cli_dns.py +++ b/erdpy/cli_dns.py @@ -2,9 +2,10 @@ from prettytable import PrettyTable from erdpy import cli_shared -from erdpy.dns import name_hash, dns_address_for_name, register, resolve, registration_cost, validate_name, version, compute_dns_address_for_shard_id +from erdpy.dns import (name_hash, dns_address_for_name, register, resolve, registration_cost, + validate_name, version, compute_dns_address_for_shard_id) from erdpy.accounts import Address -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider def setup_parser(args: List[str], subparsers: Any) -> Any: @@ -66,13 +67,13 @@ def _add_name_arg(sub: Any): def dns_resolve(args: Any): - addr = resolve(args.name, ElrondProxy(args.proxy)) + addr = resolve(args.name, ProxyNetworkProvider(args.proxy)) if addr.hex() != Address.zero().hex(): print(addr.bech32()) def dns_validate_name(args: Any): - validate_name(args.name, args.shard_id, ElrondProxy(args.proxy)) + validate_name(args.name, args.shard_id, ProxyNetworkProvider(args.proxy)) def get_name_hash(args: Any): @@ -92,11 +93,11 @@ def get_dns_address_for_name_hex(args: Any): def get_registration_cost(args: Any): - print(registration_cost(args.shard_id, ElrondProxy(args.proxy))) + print(registration_cost(args.shard_id, ProxyNetworkProvider(args.proxy))) def get_version(args: Any): - proxy = ElrondProxy(args.proxy) + proxy = ProxyNetworkProvider(args.proxy) if args.all: t = PrettyTable(['Shard ID', 'Contract address (bech32)', 'Contract address (hex)', 'Version']) for shard_id in range(0, 256): diff --git a/erdpy/cli_ledger.py b/erdpy/cli_ledger.py index fd40a503..47b78e50 100644 --- a/erdpy/cli_ledger.py +++ b/erdpy/cli_ledger.py @@ -20,7 +20,7 @@ def setup_parser(subparsers: Any) -> Any: return subparsers -def print_addresses(args): +def print_addresses(args: Any): ledger_app = ElrondLedgerApp() for i in range(args.num_addresses): address = ledger_app.get_address(0, i) @@ -28,7 +28,7 @@ def print_addresses(args): ledger_app.close() -def print_version(args): +def print_version(args: Any): ledger_app = ElrondLedgerApp() print("Elrond App version: " + ledger_app.get_version()) ledger_app.close() diff --git a/erdpy/cli_network.py b/erdpy/cli_network.py index ea1b0b00..78a38a41 100644 --- a/erdpy/cli_network.py +++ b/erdpy/cli_network.py @@ -1,4 +1,4 @@ -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider from erdpy import cli_shared import logging from typing import Any @@ -27,26 +27,26 @@ def setup_parser(subparsers: Any) -> Any: return subparsers -def get_num_shards(args): +def get_num_shards(args: Any): proxy_url = args.proxy - proxy = ElrondProxy(proxy_url) - num_shards = proxy.get_num_shards() + proxy = ProxyNetworkProvider(proxy_url) + num_shards = proxy.get_network_config().num_shards_without_meta print(num_shards) return num_shards -def get_last_block_nonce(args): +def get_last_block_nonce(args: Any): proxy_url = args.proxy shard = args.shard - proxy = ElrondProxy(proxy_url) - nonce = proxy.get_last_block_nonce(shard) + proxy = ProxyNetworkProvider(proxy_url) + nonce = proxy.get_network_status(shard).highest_final_nonce print(nonce) return nonce -def get_chain_id(args): +def get_chain_id(args: Any): proxy_url = args.proxy - proxy = ElrondProxy(proxy_url) - chain_id = proxy.get_chain_id() + proxy = ProxyNetworkProvider(proxy_url) + chain_id = proxy.get_network_config().chain_id print(chain_id) return chain_id diff --git a/erdpy/cli_password.py b/erdpy/cli_password.py index 3412d4ca..f05b54a2 100644 --- a/erdpy/cli_password.py +++ b/erdpy/cli_password.py @@ -1,6 +1,7 @@ +from typing import Any from getpass import getpass -def load_password(args): +def load_password(args: Any): if args.passfile: with open(args.passfile) as pass_file: return pass_file.read().strip() diff --git a/erdpy/cli_shared.py b/erdpy/cli_shared.py index c0bb1cca..7060c756 100644 --- a/erdpy/cli_shared.py +++ b/erdpy/cli_shared.py @@ -9,7 +9,7 @@ from erdpy.cli_output import CLIOutputBuilder from erdpy.cli_password import load_password from erdpy.ledger.ledger_functions import do_get_ledger_address -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider from erdpy.simulation import Simulator from erdpy.transactions import Transaction @@ -125,7 +125,7 @@ def prepare_nonce_in_args(args: Any): else: raise errors.NoWalletProvided() - account.sync_nonce(ElrondProxy(args.proxy)) + account.sync_nonce(ProxyNetworkProvider(args.proxy)) args.nonce = account.nonce @@ -146,7 +146,7 @@ def check_broadcast_args(args: Any): def send_or_simulate(tx: Transaction, args: Any, dump_output: bool = True) -> CLIOutputBuilder: - proxy = ElrondProxy(args.proxy) + proxy = ProxyNetworkProvider(args.proxy) is_set_wait_result = hasattr(args, "wait_result") and args.wait_result is_set_send = hasattr(args, "send") and args.send @@ -178,7 +178,7 @@ def send_or_simulate(tx: Transaction, args: Any, dump_output: bool = True) -> CL def check_if_sign_method_required(args: List[str], checked_method: str) -> bool: methods = ["--pem", "--keyfile", "--ledger"] - rest_of_methods = [] + rest_of_methods: List[str] = [] for method in methods: if method != checked_method: rest_of_methods.append(method) diff --git a/erdpy/cli_transactions.py b/erdpy/cli_transactions.py index 51162a6b..6eb02fcf 100644 --- a/erdpy/cli_transactions.py +++ b/erdpy/cli_transactions.py @@ -3,7 +3,7 @@ from erdpy import cli_shared, utils from erdpy.cli_output import CLIOutputBuilder -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider from erdpy.transactions import Transaction, do_prepare_transaction @@ -70,7 +70,7 @@ def send_transaction(args: Any): tx = Transaction.load_from_file(args.infile) try: - tx.send(ElrondProxy(args.proxy)) + tx.send(ProxyNetworkProvider(args.proxy)) finally: output = CLIOutputBuilder().set_emitted_transaction(tx).build() utils.dump_out_json(output, outfile=args.outfile) @@ -79,8 +79,8 @@ def send_transaction(args: Any): def get_transaction(args: Any): args = utils.as_object(args) omit_fields = cli_shared.parse_omit_fields_arg(args) - proxy = ElrondProxy(args.proxy) + proxy = ProxyNetworkProvider(args.proxy) - transaction = proxy.get_transaction(args.hash, args.sender, args.with_results) + transaction = proxy.get_transaction(args.hash) output = CLIOutputBuilder().set_transaction_on_network(transaction, omit_fields).build() utils.dump_out_json(output) diff --git a/erdpy/config.py b/erdpy/config.py index 9c1264a3..987bc03d 100644 --- a/erdpy/config.py +++ b/erdpy/config.py @@ -153,7 +153,7 @@ def get_defaults() -> Dict[str, Any]: "chainID": "T", "txVersion": "1", "dependencies.vmtools.tag": "latest", - "dependencies.elrond_wasm_rs.tag": "latest", + "dependencies.mx_sdk_rs.tag": "latest", "dependencies.vmtools.urlTemplate.linux": "https://github.com/ElrondNetwork/wasm-vm/archive/{TAG}.tar.gz", "dependencies.vmtools.urlTemplate.osx": "https://github.com/ElrondNetwork/wasm-vm/archive/{TAG}.tar.gz", "dependencies.llvm.tag": "v9-19feb", diff --git a/erdpy/contracts.py b/erdpy/contracts.py index bca8b124..bdd7021c 100644 --- a/erdpy/contracts.py +++ b/erdpy/contracts.py @@ -1,14 +1,14 @@ import base64 import logging -from typing import Any, List, Optional, Tuple +from typing import Any, List, Optional, Tuple, Protocol, Sequence from Cryptodome.Hash import keccak from erdpy import config, constants, errors from erdpy.accounts import Account, Address -from erdpy.interfaces import IElrondProxy from erdpy.transactions import Transaction from erdpy.utils import Object +from erdpy_network_providers.interface import IContractQuery logger = logging.getLogger("contracts") @@ -19,6 +19,11 @@ STR_PREFIX = "str:" +class INetworkProvider(Protocol): + def query_contract(self, query: Any) -> Any: + ... + + class QueryResult(Object): def __init__(self, as_base64: str, as_hex: str, as_number: int): self.base64 = as_base64 @@ -26,6 +31,30 @@ def __init__(self, as_base64: str, as_hex: str, as_number: int): self.number = as_number +class ContractQuery(IContractQuery): + def __init__(self, address: Address, function: str, value: int, arguments: List[bytes], caller: Optional[Address] = None): + self.contract = address + self.function = function + self.caller = caller + self.value = value + self.encoded_arguments = [item.hex() for item in arguments] + + def get_contract(self) -> Address: + return self.contract + + def get_function(self) -> str: + return self.function + + def get_encoded_arguments(self) -> Sequence[str]: + return self.encoded_arguments + + def get_caller(self) -> Optional[Address]: + return self.caller + + def get_value(self) -> int: + return self.value + + class SmartContract: def __init__(self, address: Optional[Address] = None, bytecode=None, metadata=None): self.address = Address(address) @@ -136,33 +165,24 @@ def prepare_upgrade_transaction_data(self, arguments: List[Any]): def query( self, - proxy: IElrondProxy, + proxy: INetworkProvider, function: str, arguments: List[Any], value: int = 0, caller: Optional[Address] = None ) -> List[Any]: response_data = self.query_detailed(proxy, function, arguments, value, caller) - return_data = response_data.get("returnData", []) or response_data.get("ReturnData", []) + return_data = response_data.return_data return [self._interpret_return_data(data) for data in return_data] - def query_detailed(self, proxy: IElrondProxy, function: str, arguments: List[Any], value: int = 0, caller: Optional[Address] = None) -> Any: + def query_detailed(self, proxy: INetworkProvider, function: str, arguments: List[Any], + value: int = 0, caller: Optional[Address] = None) -> Any: arguments = arguments or [] - prepared_arguments = [_prepare_argument(argument) for argument in arguments] - - payload = { - "scAddress": self.address.bech32(), - "funcName": function, - "args": prepared_arguments, - "value": str(value) - } - if caller: - payload["caller"] = caller.bech32() + query = ContractQuery(self.address, function, value, arguments, caller) - response = proxy.query_contract(payload) - response_data = response.get("data", {}) - return response_data + response = proxy.query_contract(query) + return response def _interpret_return_data(self, data: str) -> Any: if not data: diff --git a/erdpy/dependencies/modules.py b/erdpy/dependencies/modules.py index 679a3f18..e6aa6817 100644 --- a/erdpy/dependencies/modules.py +++ b/erdpy/dependencies/modules.py @@ -150,8 +150,8 @@ def __init__(self, key: str, aliases: List[str] = None): aliases = list() super().__init__(key, aliases) - self.repo_name = 'wasm-vm' - self.organisation = 'ElrondNetwork' + self.repo_name = 'mx-chain-vm-go' + self.organisation = 'multiversx' def _post_install(self, tag: str): dependencies.install_module('golang') @@ -406,8 +406,8 @@ def _post_install(self, tag: str): class TestWalletsModule(StandaloneModule): def __init__(self, key: str): super().__init__(key, []) - self.organisation = "ElrondNetwork" - self.repo_name = "elrond-sdk-testwallets" + self.organisation = "multiversx" + self.repo_name = "mx-sdk-testwallets" def _post_install(self, tag: str): # We'll create a "latest" symlink diff --git a/erdpy/dns.py b/erdpy/dns.py index 6d87d44a..ba0d2930 100644 --- a/erdpy/dns.py +++ b/erdpy/dns.py @@ -1,20 +1,23 @@ -from typing import Any, List +from typing import Any, List, Protocol from Cryptodome.Hash import keccak from erdpy import cli_shared, utils from erdpy.accounts import Account, Address from erdpy.contracts import SmartContract -from erdpy.proxy.core import ElrondProxy from erdpy.transactions import do_prepare_transaction -from erdpy.interfaces import IElrondProxy MaxNumShards = 256 ShardIdentiferLen = 2 InitialDNSAddress = bytes([1] * 32) -def resolve(name: str, proxy: ElrondProxy) -> Address: +class INetworkProvider(Protocol): + def query_contract(self, query: Any) -> Any: + ... + + +def resolve(name: str, proxy: INetworkProvider) -> Address: name_arg = "0x{}".format(str.encode(name).hex()) dns_address = dns_address_for_name(name) contract = SmartContract(dns_address) @@ -24,7 +27,7 @@ def resolve(name: str, proxy: ElrondProxy) -> Address: return Address(result[0].hex) -def validate_name(name: str, shard_id: int, proxy: ElrondProxy): +def validate_name(name: str, shard_id: int, proxy: INetworkProvider): name_arg = "0x{}".format(str.encode(name).hex()) dns_address = compute_dns_address_for_shard_id(shard_id) contract = SmartContract(dns_address) @@ -49,7 +52,7 @@ def register(args: Any): def compute_all_dns_addresses() -> List[Address]: - addresses = [] + addresses: List[Address] = [] for i in range(0, 256): addresses.append(compute_dns_address_for_shard_id(i)) return addresses @@ -59,7 +62,7 @@ def name_hash(name: str) -> bytes: return keccak.new(digest_bits=256).update(str.encode(name)).digest() -def registration_cost(shard_id: int, proxy: ElrondProxy) -> int: +def registration_cost(shard_id: int, proxy: INetworkProvider) -> int: dns_address = compute_dns_address_for_shard_id(shard_id) contract = SmartContract(dns_address) result = contract.query(proxy, "getRegistrationCost", []) @@ -69,7 +72,7 @@ def registration_cost(shard_id: int, proxy: ElrondProxy) -> int: return int("0x{}".format(result[0])) -def version(shard_id: int, proxy: IElrondProxy) -> str: +def version(shard_id: int, proxy: INetworkProvider) -> str: dns_address = compute_dns_address_for_shard_id(shard_id) contract = SmartContract(dns_address) result = contract.query(proxy, "version", []) diff --git a/erdpy/interfaces.py b/erdpy/interfaces.py index 5b09f9ac..8d27636f 100644 --- a/erdpy/interfaces.py +++ b/erdpy/interfaces.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, List, Tuple +from typing import Any, Dict from erdpy.utils import ISerializable @@ -44,43 +44,9 @@ def sign_transaction(self, transaction: ITransaction) -> str: return "" -class ITransactionOnNetwork(ISerializable): - def is_done(self) -> bool: - return False - - def get_hash(self) -> str: - return "" - - class ISimulateResponse(ISerializable): pass class ISimulateCostResponse(ISerializable): pass - - -class IElrondProxy: - def get_account_nonce(self, address: IAddress) -> int: - return 0 - - def get_transaction(self, tx_hash: str, sender_address: str = "", with_results: bool = False) -> ITransactionOnNetwork: - return ITransactionOnNetwork() - - def send_transaction(self, payload: Any) -> str: - return "" - - def simulate_transaction(self, payload: Any) -> ISimulateResponse: - return ISimulateResponse() - - def simulate_transaction_cost(self, payload: Any) -> ISimulateCostResponse: - return ISimulateCostResponse() - - def send_transactions(self, payload: List[Any]) -> Tuple[int, List[str]]: - return 0, [] - - def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout: int) -> ITransactionOnNetwork: - return ITransactionOnNetwork() - - def query_contract(self, payload: Any) -> Any: - return dict() diff --git a/erdpy/projects/eei_activation.py b/erdpy/projects/eei_activation.py index b805416b..fdf4adc9 100644 --- a/erdpy/projects/eei_activation.py +++ b/erdpy/projects/eei_activation.py @@ -5,7 +5,7 @@ import requests import toml from erdpy.diskcache import DiskCache -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider logger = logging.getLogger("eei") @@ -28,8 +28,8 @@ def is_flag_active(self, flag_name: str): def _fetch_current_epoch(self): logger.info(f"fetch_current_epoch: {self.proxy_url}") - proxy = ElrondProxy(self.proxy_url) - return proxy.get_epoch() + proxy = ProxyNetworkProvider(self.proxy_url) + return proxy.get_network_status().epoch_number def _fetch_enable_epochs(self): logger.info(f"fetch_enable_epochs: {self.enable_epochs_url}") diff --git a/erdpy/projects/templates_config.py b/erdpy/projects/templates_config.py index c278e6b1..e2ef3553 100644 --- a/erdpy/projects/templates_config.py +++ b/erdpy/projects/templates_config.py @@ -7,26 +7,26 @@ def get_templates_repositories(): timestamp = int(time.time()) - examples_rs_tag = config.get_dependency_tag('elrond_wasm_rs') + examples_rs_tag = config.get_dependency_tag('mx_sdk_rs') if examples_rs_tag == 'latest': - examples_rs_tag = query_latest_release_tag('ElrondNetwork/elrond-wasm-rs') + examples_rs_tag = query_latest_release_tag('multiversx/mx-sdk-rs') examples_rs_tag_no_v = remove_initial_v_from_version(examples_rs_tag) return [ TemplatesRepository( key="sc-examples", - url=f"https://github.com/ElrondNetwork/sc-examples/archive/master.zip?t={timestamp}", - github="ElrondNetwork/sc-examples", - relative_path="sc-examples-master" + url=f"https://github.com/multiversx/mx-sc-examples/archive/master.zip?t={timestamp}", + github="multiversx/mx-sc-examples", + relative_path="mx-sc-examples-master" ), TemplatesRepository( - key="elrond-wasm-rs", - url=f"https://github.com/ElrondNetwork/elrond-wasm-rs/archive/{examples_rs_tag}.zip?t={timestamp}", - github="ElrondNetwork/elrond-wasm-rs", - relative_path=f"elrond-wasm-rs-{examples_rs_tag_no_v}/contracts/examples" + key="mx-sdk-rs", + url=f"https://github.com/multiversx/mx-sdk-rs/archive/{examples_rs_tag}.zip?t={timestamp}", + github="multiversx/mx-sdk-rs", + relative_path=f"mx-sdk-rs-{examples_rs_tag_no_v}/contracts/examples" ) ] diff --git a/erdpy/proxy/__init__.py b/erdpy/proxy/__init__.py deleted file mode 100644 index c8c111f9..00000000 --- a/erdpy/proxy/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from erdpy.proxy.core import ElrondProxy - -__all__ = ["ElrondProxy"] diff --git a/erdpy/proxy/core.py b/erdpy/proxy/core.py deleted file mode 100644 index 8388a95a..00000000 --- a/erdpy/proxy/core.py +++ /dev/null @@ -1,167 +0,0 @@ -import logging -import time -from typing import Any, List, Tuple, Union, cast - -from erdpy.accounts import Address -from erdpy.interfaces import IAddress, IElrondProxy, ISimulateCostResponse, ISimulateResponse, ITransactionOnNetwork -from erdpy.proxy.http_facade import do_get, do_post -from erdpy.proxy.messages import NetworkConfig, SimulateCostResponse, SimulateResponse, TransactionOnNetwork - -METACHAIN_ID = 4294967295 -ANY_SHARD_ID = 0 -AWAIT_TRANSACTION_PERIOD = 5 - -logger = logging.getLogger("proxy") - - -class ElrondProxy(IElrondProxy): - def __init__(self, url: str): - self.url = url - - def get_account_nonce(self, address: IAddress) -> int: - url = f"{self.url}/address/{address.bech32()}" - response = do_get(url) - nonce = response.get("account").get("nonce", 0) - return int(nonce) - - def get_account_balance(self, address: IAddress): - url = f"{self.url}/address/{address.bech32()}/balance" - response = do_get(url) - balance = response.get("balance", 0) - return int(balance) - - def get_account(self, address: IAddress): - url = f"{self.url}/address/{address.bech32()}" - response = do_get(url) - account = response.get("account", dict()) - return account - - def get_account_transactions(self, address: Address): - TRUNCATE_DATA_THRESHOLD = 75 - - url = f"{self.url}/address/{address.bech32()}/transactions" - response = do_get(url) - transactions = response.get("transactions", []) - for transaction in transactions: - data = transaction.get("data") or "" - data = (data[:TRUNCATE_DATA_THRESHOLD] + ' ... truncated ...') if len(data) > TRUNCATE_DATA_THRESHOLD else data - transaction["data"] = data - return transactions - - def get_esdt_tokens(self, address: str) -> List: - response = do_get(f"{self.url}/address/{address}/esdt") - esdts = response.get("esdts") - return cast(List, esdts) - - def get_esdt_balance(self, address: str, ticker: str) -> dict: - response = do_get(f"{self.url}/address/{address}/esdt/{ticker}") - token_data = response.get("tokenData") - return cast(dict, token_data) - - def get_all_tokens(self) -> List[str]: - response = do_get(f"{self.url}/network/esdts") - tokens = response.get("tokens", []) - return cast(List[str], tokens) - - def get_num_shards(self): - network_config = self.get_network_config() - return network_config.num_shards - - def get_epoch(self): - status = self._get_network_status(METACHAIN_ID) - nonce = status.get("erd_epoch_number", 0) - return nonce - - def get_last_block_nonce(self, shard_id: Union[str, int]): - if shard_id == "metachain": - metrics = self._get_network_status(METACHAIN_ID) - else: - metrics = self._get_network_status(shard_id) - - nonce = metrics.get("erd_highest_final_nonce", 0) - return nonce - - def get_gas_price(self): - network_config = self.get_network_config() - return network_config.min_gas_price - - def get_chain_id(self): - network_config = self.get_network_config() - return network_config.chain_id - - def _get_network_status(self, shard_id: Union[str, int]): - url = f"{self.url}/network/status/{shard_id}" - response = do_get(url) - payload = response.get("status") - return payload - - def get_network_config(self) -> NetworkConfig: - url = f"{self.url}/network/config" - response = do_get(url) - payload = response.get("config") - result = NetworkConfig(payload) - return result - - def send_transaction(self, payload: Any) -> str: - url = f"{self.url}/transaction/send" - response = do_post(url, payload) - tx_hash = str(response.get("txHash")) - return tx_hash - - def simulate_transaction(self, payload: Any) -> ISimulateResponse: - url = f"{self.url}/transaction/simulate" - response = do_post(url, payload) - return SimulateResponse(response) - - def simulate_transaction_cost(self, payload: Any) -> ISimulateCostResponse: - url = f"{self.url}/transaction/cost" - response = do_post(url, payload) - return SimulateCostResponse(response) - - def send_transactions(self, payload: List[Any]) -> Tuple[int, List[str]]: - url = f"{self.url}/transaction/send-multiple" - response = do_post(url, payload) - # Proxy and Observers have different response format: - num_sent = response.get("numOfSentTxs", 0) or response.get("txsSent", 0) - hashes = response.get("txsHashes") - return num_sent, hashes - - def query_contract(self, payload: Any) -> Any: - url = f"{self.url}/vm-values/query" - response = do_post(url, payload) - return response - - def get_transaction(self, tx_hash: str, sender_address: str = "", with_results: bool = False) -> TransactionOnNetwork: - url = f"{self.url}/transaction/{tx_hash}" - url += f"?sender={sender_address or ''}" - url += f"&withResults={with_results}" - - response = do_get(url) - return TransactionOnNetwork(tx_hash, response) - - def get_hyperblock(self, key) -> Any: - url = f"{self.url}/hyperblock/by-hash/{key}" - if str(key).isnumeric(): - url = f"{self.url}/hyperblock/by-nonce/{key}" - - response = do_get(url) - response = response.get("hyperblock", {}) - return response - - def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout: int = 100) -> ITransactionOnNetwork: - url = f"{self.url}/transaction/send" - response = do_post(url, payload) - tx_hash = response.get("txHash") - num_periods_to_wait = int(num_seconds_timeout / AWAIT_TRANSACTION_PERIOD) - - for _ in range(0, num_periods_to_wait): - time.sleep(AWAIT_TRANSACTION_PERIOD) - - tx = self.get_transaction(tx_hash=tx_hash, with_results=True) - if tx.is_done(): - return tx - else: - logger.info("Transaction not yet done.") - - return ITransactionOnNetwork() - diff --git a/erdpy/proxy/http_facade.py b/erdpy/proxy/http_facade.py deleted file mode 100644 index 02aab53b..00000000 --- a/erdpy/proxy/http_facade.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Any, Dict -import requests -from erdpy import errors -from erdpy.proxy.messages import GenericProxyResponse - - -def do_get(url: str) -> GenericProxyResponse: - try: - response = requests.get(url) - response.raise_for_status() - parsed = response.json() - return get_data(parsed, url) - except requests.HTTPError as err: - error_data = _extract_error_from_response(err.response) - raise errors.ProxyRequestError(url, error_data) - except requests.ConnectionError as err: - raise errors.ProxyRequestError(url, err) - except Exception as err: - raise errors.ProxyRequestError(url, err) - - -def do_post(url: str, payload: Any) -> GenericProxyResponse: - try: - response = requests.post(url, json=payload) - response.raise_for_status() - parsed = response.json() - return get_data(parsed, url) - except requests.HTTPError as err: - error_data = _extract_error_from_response(err.response) - raise errors.ProxyRequestError(url, error_data) - except requests.ConnectionError as err: - raise errors.ProxyRequestError(url, err) - except Exception as err: - raise errors.ProxyRequestError(url, err) - - -def get_data(parsed: Dict[str, Any], url: str) -> GenericProxyResponse: - err = parsed.get("error") - code = parsed.get("code") - - if not err and code == "successful": - data: Dict[str, Any] = parsed.get("data", dict()) - return GenericProxyResponse(data) - - raise errors.ProxyRequestError(url, f"code:{code}, error: {err}") - - -def _extract_error_from_response(response: Any): - try: - return response.json() - except Exception: - return response.text diff --git a/erdpy/proxy/messages.py b/erdpy/proxy/messages.py deleted file mode 100644 index 72dedd8b..00000000 --- a/erdpy/proxy/messages.py +++ /dev/null @@ -1,141 +0,0 @@ -import base64 -from typing import Any, Dict, List, Union - -from erdpy.interfaces import ISimulateCostResponse, ISimulateResponse, ITransactionOnNetwork -from erdpy.utils import ISerializable - - -class GenericProxyResponse(ISerializable): - def __init__(self, data: Any) -> None: - self.__dict__.update(data) - - def get(self, key: str, default: Any = None) -> Any: - return self.__dict__.get(key, default) - - -class NetworkConfig: - def __init__(self, data: Dict[str, Any]) -> None: - self.num_shards = data.get("erd_num_shards_without_meta", 0) - self.min_gas_price = data.get("erd_min_gas_price", 0) - self.chain_id = data.get("erd_chain_id", "?") - self.min_tx_version = data.get("erd_min_transaction_version", 0) - - -class TransactionOnNetwork(ITransactionOnNetwork): - def __init__(self, hash: str, response: GenericProxyResponse) -> None: - raw = response.get("transaction", dict()) - contract_results: List[Dict[str, Any]] = raw.get("smartContractResults", []) - - self.raw = raw - self.hash = hash - self.parsed_contract_results = [SmartContractResult(item) for item in contract_results] - self.parsed_logs: List[Log] = [] - - def is_done(self) -> bool: - hyperblock: int = self.raw.get("hyperblockNonce", 0) - return hyperblock > 0 - - def get_hash(self) -> str: - return self.hash - - def to_dictionary(self) -> Dict[str, Any]: - result: Dict[str, Any] = dict() - result.update(self.raw) - result["parsed"] = dict() - - if self.parsed_contract_results: - result["parsed"]["smartContractResults"] = self.parsed_contract_results - if self.parsed_logs: - result["parsed"]["logs"] = self.parsed_logs - - return result - - -class SmartContractResult(ISerializable): - def __init__(self, raw: Dict[str, Any]) -> None: - self.raw = raw - self.return_message = self._parse_return_message() - self.arguments = self._parse_arguments() - self.parsed_log = Log(raw.get("logs", {})) - - def _parse_return_message(self) -> str: - try: - data_parts = self._parse_data_parts() - return_message_encoded = data_parts[0] - return_message = bytes.fromhex(return_message_encoded).decode("ascii") - return return_message - except: - return "" - - def _parse_arguments(self) -> List[str]: - try: - data_parts = self._parse_data_parts() - arguments = data_parts[1:] - return arguments - except: - return [] - - def _parse_data_parts(self) -> List[str]: - data: str = self.raw.get("data", "").lstrip("@") - return data.split("@") - - def to_dictionary(self) -> Dict[str, Any]: - result: Dict[str, Any] = dict() - result.update(self.raw) - result["parsed"] = { - "returnMessage": self.return_message, - "arguments": self.arguments, - "log": self.parsed_log - } - - return result - - -class Log(ISerializable): - def __init__(self, raw: Dict[str, Any]) -> None: - self.raw = raw - - -class SimulateResponse(ISimulateResponse): - def __init__(self, response: GenericProxyResponse) -> None: - result: Dict[str, Any] = response.get("result") or dict() - contract_results: Dict[str, Any] = result.get("scResults") or dict() - - self.raw = response.to_dictionary() - self.parsed_contract_results = [SmartContractResult(item) for item in contract_results.values()] - - def to_dictionary(self) -> Dict[str, Any]: - result: Dict[str, Any] = dict() - result.update(self.raw) - result["parsed"] = dict() - - if self.parsed_contract_results: - result["parsed"]["smartContractResults"] = self.parsed_contract_results - - return result - - -class SimulateCostResponse(ISimulateCostResponse): - def __init__(self, response: GenericProxyResponse) -> None: - contract_results: Dict[str, Any] = response.get("smartContractResults") or dict() - - self.raw = response.to_dictionary() - self.parsed_contract_results = [SmartContractResult(item) for item in contract_results.values()] - - def to_dictionary(self) -> Dict[str, Any]: - result: Dict[str, Any] = dict() - result.update(self.raw) - result["parsed"] = dict() - - if self.parsed_contract_results: - result["parsed"]["smartContractResults"] = self.parsed_contract_results - - return result - - -def decode_hex_base64(input: Union[str, None]) -> str: - return bytes.fromhex(decode_base64(input)).decode('ascii') if input else "" - - -def decode_base64(input: Union[str, None]) -> str: - return base64.b64decode(input).decode() if input else "" diff --git a/erdpy/simulation.py b/erdpy/simulation.py index b1a345b8..e7f352fa 100644 --- a/erdpy/simulation.py +++ b/erdpy/simulation.py @@ -1,9 +1,17 @@ from collections import OrderedDict -from typing import Any, Dict -from erdpy.interfaces import IElrondProxy, ISimulateCostResponse, ISimulateResponse, ITransaction +from typing import Any, Dict, Protocol +from erdpy.interfaces import ISimulateCostResponse, ISimulateResponse, ITransaction from erdpy.utils import ISerializable +class INetworkProvider(Protocol): + def simulate_transaction(self, transaction: Dict[str, Any]) -> ISimulateResponse: + ... + + def simulate_transaction_cost(self, transaction: Dict[str, Any]) -> ISimulateCostResponse: + ... + + class Simulation(ISerializable): def __init__(self, simulate_response: ISimulateResponse, simulate_cost_response: ISimulateCostResponse) -> None: self.simulation_response = simulate_response @@ -17,7 +25,7 @@ def to_dictionary(self) -> Dict[str, Any]: return dictionary class Simulator(): - def __init__(self, proxy: IElrondProxy) -> None: + def __init__(self, proxy: INetworkProvider) -> None: self.proxy = proxy def run(self, transaction: ITransaction) -> Simulation: diff --git a/erdpy/tests/test_accounts_repository.py b/erdpy/tests/test_accounts_repository.py index 9329e9ac..a9af78f2 100644 --- a/erdpy/tests/test_accounts_repository.py +++ b/erdpy/tests/test_accounts_repository.py @@ -1,12 +1,19 @@ -from erdpy.accounts import Address +from typing import Protocol + +from erdpy.accounts import Address, AccountOnNetwork from erdpy.accounts_repository import AccountsRepository -from erdpy.interfaces import IAddress, IElrondProxy +from erdpy.interfaces import IAddress from erdpy.workstation import get_tools_folder TESTNET_USERS_FOLDER = get_tools_folder() / "testwallets" / "latest" / "users" DUMMY_NONCE = 42 +class INetworkProvider(Protocol): + def get_account(self, address: IAddress) -> AccountOnNetwork: + ... + + def test_create_from_folder(): repository = AccountsRepository.create_from_folder(TESTNET_USERS_FOLDER) assert len(repository) == 12 @@ -49,6 +56,9 @@ def test_sync_nonces(): assert account.nonce == DUMMY_NONCE -class ElrondProxyStub(IElrondProxy): - def get_account_nonce(self, address: IAddress) -> int: - return DUMMY_NONCE +class ElrondProxyStub(INetworkProvider): + def get_account(self, address: IAddress) -> AccountOnNetwork: + account = AccountOnNetwork() + account.nonce = DUMMY_NONCE + + return account diff --git a/erdpy/tests/test_proxy.py b/erdpy/tests/test_proxy.py new file mode 100644 index 00000000..94e4ec79 --- /dev/null +++ b/erdpy/tests/test_proxy.py @@ -0,0 +1,140 @@ +from pathlib import Path +from erdpy.cli import main +from erdpy.accounts import Account +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider + + +def test_get_transactions(): + output_file = Path(__file__).parent / "testdata-out" / "transactions.txt" + + main( + [ + "account", + "get-transactions", + "--address", + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + "--outfile", + str(output_file), + ] + ) + assert Path.is_file(output_file) == True + + +def test_get_account(): + result = main( + [ + "account", + "get", + "--address", + "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", + ] + ) + if not result: + assert True + else: + assert False + + +def test_get_hyperblock_by_nonce(): + result = main( + [ + "hyperblock", + "get", + "--key", + "3895403", + "--proxy", + "https://devnet-api.elrond.com", + ] + ) + if not result: + assert True + else: + assert False + + +def test_get_hyperblock_by_hash(): + result = main( + [ + "hyperblock", + "get", + "--key", + "85ccf29f51d7acf0fbb6cfcf2e4b89eee2f2264dd989edd5a870d33dacc24743", + "--proxy", + "https://devnet-api.elrond.com", + ] + ) + if not result: + assert True + else: + assert False + + +def test_sync_nonce(): + account = Account("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") + proxy = ProxyNetworkProvider("https://devnet-api.elrond.com") + account.sync_nonce(proxy) + + assert account.nonce >= 11480 + + +def test_query_contract(): + result = main( + [ + "contract", + "query", + "erd1qqqqqqqqqqqqqpgquykqja5c4v33zdmnwglj3jphqwrelzdn396qlc9g33", + "--function", + "getSum", + "--proxy", + "https://devnet-api.elrond.com", + ] + ) + if not result: + assert True + else: + assert False + + +def test_get_num_shards(): + result = main(["network", "num-shards"]) + + if not result: + assert True + else: + assert False + + +def test_get_last_block_nonce(): + result = main(["network", "block-nonce", "--shard", "4294967295"]) + + if not result: + assert True + else: + assert False + + +def test_get_chain_id(): + result = main(["network", "chain"]) + + if not result: + assert True + else: + assert False + + +def test_get_transaction(): + result = main( + [ + "tx", + "get", + "--proxy", + "https://devnet-api.elrond.com", + "--hash", + "2cb813be9d5e5040abb2522da75fa5c8d94f72caa510ff51d7525659f398298b", + ] + ) + + if not result: + assert True + else: + assert False diff --git a/erdpy/tests/testdata-out/.gitignore b/erdpy/tests/testdata-out/.gitignore new file mode 100644 index 00000000..210accf2 --- /dev/null +++ b/erdpy/tests/testdata-out/.gitignore @@ -0,0 +1,6 @@ +# Ignore everything +* + +# But not these files... +!.gitignore + diff --git a/erdpy/transactions.py b/erdpy/transactions.py index abeec062..0fefdb1f 100644 --- a/erdpy/transactions.py +++ b/erdpy/transactions.py @@ -1,17 +1,37 @@ import base64 import json import logging +import time from collections import OrderedDict -from typing import Any, Dict, List, TextIO +from typing import Any, Dict, List, TextIO, Tuple, Protocol, Sequence from erdpy import config, errors, utils from erdpy.accounts import Account, Address, LedgerAccount from erdpy.cli_password import load_password -from erdpy.interfaces import IElrondProxy, ITransaction, ITransactionOnNetwork +from erdpy.interfaces import ITransaction logger = logging.getLogger("transactions") +class ITransactionOnNetwork(Protocol): + hash: str + is_completed: bool + + def to_dictionary(self) -> Dict[str, Any]: + ... + + +class INetworkProvider(Protocol): + def send_transaction(self, transaction: ITransaction) -> str: + ... + + def send_transactions(self, transactions: Sequence[ITransaction]) -> Tuple[int, str]: + ... + + def get_transaction(self, tx_hash: str) -> ITransactionOnNetwork: + ... + + class Transaction(ITransaction): def __init__(self): self.hash: str = "" @@ -50,7 +70,7 @@ def receiver_username_decoded(self) -> str: return self._field_decoded("receiverUsername") def _field_encoded(self, field: str) -> str: - field_bytes = self.__dict__.get(field, None).encode("utf-8") + field_bytes = self.__dict__.get(field, "").encode("utf-8") encoded = base64.b64encode(field_bytes).decode() return encoded @@ -83,7 +103,7 @@ def serialize_as_inner(self) -> str: @classmethod def load_from_file(cls, f: TextIO): data_json: bytes = f.read().encode() - fields = json.loads(data_json).get("tx") + fields = json.loads(data_json).get("tx") or json.loads(data_json).get("emittedTransaction") instance = cls() instance.__dict__.update(fields) instance.data = instance.data_decoded() @@ -91,25 +111,41 @@ def load_from_file(cls, f: TextIO): instance.receiverUsername = instance.receiver_username_encoded() return instance - def send(self, proxy: IElrondProxy): + def send(self, proxy: INetworkProvider): if not self.signature: raise errors.TransactionIsNotSigned() logger.info(f"Transaction.send: nonce={self.nonce}") - dictionary = self.to_dictionary() - self.hash = proxy.send_transaction(dictionary) + self.hash = proxy.send_transaction(self) logger.info(f"Hash: {self.hash}") utils.log_explorer_transaction(self.chainID, self.hash) return self.hash - def send_wait_result(self, proxy: IElrondProxy, timeout: int) -> ITransactionOnNetwork: + def send_wait_result(self, proxy: INetworkProvider, timeout: int) -> ITransactionOnNetwork: if not self.signature: raise errors.TransactionIsNotSigned() - txOnNetwork = proxy.send_transaction_and_wait_for_result(self.to_dictionary(), timeout) - self.hash = txOnNetwork.get_hash() + txOnNetwork = self.__send_transaction_and_wait_for_result(proxy , self, timeout) + self.hash = txOnNetwork.hash return txOnNetwork + + def __send_transaction_and_wait_for_result(self, proxy: INetworkProvider, payload: Any, num_seconds_timeout: int = 100) -> ITransactionOnNetwork: + AWAIT_TRANSACTION_PERIOD = 5 + + tx_hash = proxy.send_transaction(payload) + num_periods_to_wait = int(num_seconds_timeout / AWAIT_TRANSACTION_PERIOD) + + for _ in range(0, num_periods_to_wait): + time.sleep(AWAIT_TRANSACTION_PERIOD) + + tx = proxy.get_transaction(tx_hash) + if tx.is_completed: + return tx + else: + logger.info("Transaction not yet done.") + + raise errors.KnownError("Took too long to get transaction.") def to_dictionary(self) -> Dict[str, Any]: dictionary: Dict[str, Any] = OrderedDict() @@ -194,13 +230,12 @@ def add(self, sender: Account, receiver_address: str, nonce: Any, value: Any, da tx.sign(sender) self.transactions.append(tx) - def add_tx(self, tx): + def add_tx(self, tx: Transaction): self.transactions.append(tx) - def send(self, proxy: IElrondProxy): + def send(self, proxy: INetworkProvider): logger.info(f"BunchOfTransactions.send: {len(self.transactions)} transactions") - payload = [transaction.to_dictionary() for transaction in self.transactions] - num_sent, hashes = proxy.send_transactions(payload) + num_sent, hashes = proxy.send_transactions(self.transactions) logger.info(f"Sent: {num_sent}") logger.info(f"TxsHashes: {hashes}") return num_sent, hashes @@ -221,8 +256,8 @@ def do_prepare_transaction(args: Any) -> Transaction: tx.value = args.value tx.receiver = args.receiver tx.sender = account.address.bech32() - tx.senderUsername = getattr(args, "sender_username", None) - tx.receiverUsername = getattr(args, "receiver_username", None) + tx.senderUsername = getattr(args, "sender_username", "") + tx.receiverUsername = getattr(args, "receiver_username", "") tx.gasPrice = int(args.gas_price) tx.gasLimit = int(args.gas_limit) tx.data = args.data diff --git a/erdpy/utils.py b/erdpy/utils.py index a7a639b1..1b8e765b 100644 --- a/erdpy/utils.py +++ b/erdpy/utils.py @@ -9,7 +9,7 @@ import tarfile import zipfile from pathlib import Path -from typing import Any, List, Union, Optional, cast, IO, Dict +from typing import Any, List, Union, Optional, cast, IO, Dict, Protocol, runtime_checkable import toml @@ -18,7 +18,8 @@ logger = logging.getLogger("utils") -class ISerializable: +@runtime_checkable +class ISerializable(Protocol): def to_dictionary(self) -> Dict[str, Any]: return self.__dict__ diff --git a/examples/airdrop.py b/examples/airdrop.py index bf9107cc..e9e250f7 100644 --- a/examples/airdrop.py +++ b/examples/airdrop.py @@ -6,8 +6,7 @@ from erdpy.accounts import Account, Address from erdpy.accounts_repository import AccountsRepository -from erdpy.proxy import ElrondProxy -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider from erdpy.transactions import BunchOfTransactions, Transaction @@ -31,7 +30,7 @@ def main(): parser.add_argument("--value", type=int, help="value, as a number (atoms of EGLD)") args = parser.parse_args() - proxy = ElrondProxy(args.proxy) + proxy = ProxyNetworkProvider(args.proxy) network = proxy.get_network_config() accounts = AccountsRepository.create_from_folder(Path(args.accounts)) sender = cast(Account, accounts.get_account(Address(args.sender))) @@ -51,7 +50,7 @@ def main(): transaction.gasPrice = network.min_gas_price transaction.gasLimit = 50000 transaction.chainID = network.chain_id - transaction.version = network.min_tx_version + transaction.version = network.min_transaction_version transaction.sign(sender) sender.nonce += 1 diff --git a/examples/contracts.py b/examples/contracts.py index dd22ad44..8d3c572d 100644 --- a/examples/contracts.py +++ b/examples/contracts.py @@ -5,7 +5,7 @@ from erdpy.accounts import Account, Address from erdpy.contracts import CodeMetadata, SmartContract -from erdpy.proxy.core import ElrondProxy +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider logger = logging.getLogger("examples") @@ -30,7 +30,7 @@ logging.basicConfig(level=logging.DEBUG) - proxy = ElrondProxy(args.proxy) + proxy = ProxyNetworkProvider(args.proxy) network = proxy.get_network_config() user = Account(pem_file=args.pem) @@ -49,7 +49,7 @@ def do_deploy(): gas_limit=5000000, value=0, chain=network.chain_id, - version=network.min_tx_version + version=network.min_transaction_version ) tx_on_network = tx.send_wait_result(proxy, 5000) diff --git a/examples/staking.py b/examples/staking.py index 7c37caf9..7bdf1873 100644 --- a/examples/staking.py +++ b/examples/staking.py @@ -5,8 +5,8 @@ from erdpy.accounts import Account, Address from erdpy.cli_output import CLIOutputBuilder from erdpy.cli_password import load_password -from erdpy.proxy.core import ElrondProxy from erdpy.transactions import Transaction +from erdpy_network_providers.proxy_network_provider import ProxyNetworkProvider from erdpy.validators.core import VALIDATORS_SMART_CONTRACT_ADDRESS, prepare_transaction_data_for_stake logger = logging.getLogger("examples") @@ -35,7 +35,7 @@ def main(): parser.add_argument("--value", type=int, required=True, help="value, as a number (atoms of EGLD)") args = parser.parse_args() - proxy = ElrondProxy(args.proxy) + proxy = ProxyNetworkProvider(args.proxy) network = proxy.get_network_config() password = load_password(args) node_operator = Account(key_file=args.keyfile, password=password) @@ -52,7 +52,7 @@ def main(): tx.gasLimit = gas_limit tx.data = data tx.chainID = network.chain_id - tx.version = network.min_tx_version + tx.version = network.min_transaction_version tx.sign(node_operator) utils.dump_out_json(CLIOutputBuilder().set_emitted_transaction(tx).build()) diff --git a/pyrightconfig.json b/pyrightconfig.json new file mode 100644 index 00000000..be41aee1 --- /dev/null +++ b/pyrightconfig.json @@ -0,0 +1,18 @@ +{ + "include": [ + "erdpy" + ], + "exclude": [ + "**/__pycache__" + ], + "ignore": [], + "defineConstant": { + "DEBUG": true + }, + "venvPath": ".", + "venv": ".venv", + "stubPath": "", + "reportMissingImports": true, + "reportMissingTypeStubs": false, + "reportUnknownParameterType": true +} diff --git a/requirements.txt b/requirements.txt index 52622e17..a0fbf32e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,3 +17,5 @@ pytest setuptools mx-sdk-build-contract-rs==4.0.0 + +mx-sdk-erdpy-network-providers==0.6.2 diff --git a/setup.py b/setup.py index ff091a21..b1583874 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,9 @@ import setuptools with open("README.md", "r") as fh: - long_description = "https://github.com/ElrondNetwork/elrond-sdk-erdpy" + long_description = "https://github.com/multiversx/mx-sdk-erdpy" -VERSION = "3.0.1" +VERSION = "3.0.3" try: with open('./erdpy/_version.py', 'wt') as versionfile: @@ -18,7 +18,7 @@ description="Elrond Smart Contracts Tools and Python SDK", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/ElrondNetwork/elrond-sdk-erdpy", + url="https://github.com/multiversx/mx-sdk-erdpy", author="Elrond Network", license="GPL", packages=setuptools.find_packages( @@ -36,7 +36,8 @@ "ledgercomm[hid]", "semver", "requests-cache", - "mx-sdk-build-contract-rs==4.0.0" + "mx-sdk-build-contract-rs==4.0.0", + "mx-sdk-erdpy-network-providers==0.6.2" ], zip_safe=False, keywords=["Elrond"],