diff --git a/README.md b/README.md index ed28dfcd..3cb43fb4 100644 --- a/README.md +++ b/README.md @@ -83,10 +83,13 @@ Detailed information about registering entrypoints can be found at [setuptools d ## Library structure The library provides the following primary components: + * `blockchain` - Contains helpers that allow to interact with neo blockchain, smart contracts, gas transfers, etc. * `cli` - wrappers on top of neoFS command-line tools. These wrappers execute on a shell and provide type-safe interface for interacting with the tools. * `hosting` - management of infrastructure (docker, virtual machines, services where neoFS is hosted). The library provides host implementation for docker environment (when neoFS services are running as docker containers). Support for other hosts is provided via plugins. * `reporter` - abstraction on top of test reporting tool like Allure. Components of the library will report their steps and attach artifacts to the configured reporter instance. * `shell` - shells that can be used to execute commands. Currently library provides local shell (on machine that runs the code) or SSH shell that connects to a remote machine via SSH. + * `utils` - Support functions. + ## Contributing Any contributions to the library should conform to the [contribution guideline](https://github.com/nspcc-dev/neofs-testlib/blob/master/CONTRIBUTING.md). diff --git a/pyproject.toml b/pyproject.toml index 858f235c..cb9ddb74 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ dependencies = [ "allure-python-commons>=2.9.45", "docker>=4.4.0", "importlib_metadata>=5.0; python_version < '3.10'", + "neo-mamba==0.10.0", "paramiko>=2.10.3", "pexpect>=4.8.0", "requests>=2.28.0", diff --git a/requirements.txt b/requirements.txt index 294f4063..adca8f97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ allure-python-commons==2.9.45 docker==4.4.0 importlib_metadata==5.0.0 +neo-mamba==0.10.0 paramiko==2.10.3 pexpect==4.8.0 requests==2.28.1 diff --git a/src/neofs_testlib/blockchain/__init__.py b/src/neofs_testlib/blockchain/__init__.py new file mode 100644 index 00000000..006e8f1d --- /dev/null +++ b/src/neofs_testlib/blockchain/__init__.py @@ -0,0 +1,2 @@ +from neofs_testlib.blockchain.multisig import Multisig +from neofs_testlib.blockchain.rpc_client import RPCClient diff --git a/src/neofs_testlib/blockchain/multisig.py b/src/neofs_testlib/blockchain/multisig.py new file mode 100644 index 00000000..9dafd72f --- /dev/null +++ b/src/neofs_testlib/blockchain/multisig.py @@ -0,0 +1,53 @@ +from typing import List + +from neofs_testlib.cli import NeoGo + + +class Multisig: + def __init__(self, neogo: NeoGo, invoke_tx_file: str, block_period: int): + self.neogo = neogo + self.invoke_tx_file = invoke_tx_file + self.block_period = block_period + + def create_and_send( + self, + contract_hash: str, + contract_args: str, + multisig_hash: str, + wallets: List[str], + passwords: List[str], + address: str, + endpoint: str, + ) -> None: + if not len(wallets): + raise AttributeError("Got empty wallets list") + + self.neogo.contract.invokefunction( + address=address, + rpc_endpoint=endpoint, + wallet=wallets[0], + wallet_password=passwords[0], + out=None if len(wallets) == 1 else self.invoke_tx_file, + scripthash=contract_hash, + arguments=contract_args, + multisig_hash=multisig_hash, + ) + + if len(wallets) > 1: + # sign with rest of wallets except the last one + for wallet in wallets[1:-1]: + self.neogo.wallet.sign( + wallet=wallet, + input_file=self.invoke_tx_file, + out=self.invoke_tx_file, + address=address, + ) + + # sign tx with last wallet and push it to blockchain + self.neogo.wallet.sign( + wallet=wallets[-1], + input_file=self.invoke_tx_file, + out=self.invoke_tx_file, + address=address, + rpc_endpoint=endpoint, + ) diff --git a/src/neofs_testlib/blockchain/role_designation.py b/src/neofs_testlib/blockchain/role_designation.py new file mode 100644 index 00000000..a97438e0 --- /dev/null +++ b/src/neofs_testlib/blockchain/role_designation.py @@ -0,0 +1,156 @@ +import json +from time import sleep +from typing import List, Optional + +from cli import NeoGo +from shell import Shell +from utils.converters import process_b64_bytearray + +from neofs_testlib.blockchain import Multisig + + +class RoleDesignation: + def __init__( + self, + shell: Shell, + neo_go_exec_path: str, + block_period: int, + designate_contract: str, + ): + self.neogo = NeoGo(shell, neo_go_exec_path) + self.block_period = block_period + self.designate_contract = designate_contract + + def set_notary_nodes( + self, + addr: str, + pubkeys: List[str], + script_hash: str, + wallet: str, + passwd: str, + endpoint: str, + ) -> str: + keys = [f"bytes:{k}" for k in pubkeys] + keys_str = " ".join(keys) + out = self.neogo.contract.invokefunction( + address=addr, + scripthash=self.designate_contract, + wallet=wallet, + wallet_password=passwd, + rpc_endpoint=endpoint, + arguments=f"designateAsRole int:32 [ {keys_str} ] -- {script_hash}", + force=True, + ) + sleep(self.block_period) + return out.stdout.split(" ")[-1] + + def set_inner_ring( + self, + addr: str, + pubkeys: List[str], + script_hash: str, + wallet: str, + passwd: str, + endpoint: str, + ) -> str: + keys = [f"bytes:{k}" for k in pubkeys] + keys_str = " ".join(keys) + out = self.neogo.contract.invokefunction( + address=addr, + scripthash=self.designate_contract, + wallet=wallet, + wallet_password=passwd, + rpc_endpoint=endpoint, + arguments=f"designateAsRole int:16 [ {keys_str} ] -- {script_hash}", + force=True, + ) + sleep(self.block_period) + return out.stdout.split(" ")[-1] + + def set_oracles( + self, + addr: str, + pubkeys: List[str], + script_hash: str, + wallet: str, + passwd: str, + endpoint: str, + ) -> str: + keys = [f"bytes:{k}" for k in pubkeys] + keys_str = " ".join(keys) + out = self.neogo.contract.invokefunction( + address=addr, + scripthash=self.designate_contract, + wallet=wallet, + wallet_password=passwd, + rpc_endpoint=endpoint, + arguments=f"designateAsRole int:8 [ {keys_str} ] -- {script_hash}", + force=True, + ) + sleep(self.block_period) + return out.stdout.split(" ")[-1] + + def set_notary_nodes_multisig_tx( + self, + pubkeys: List[str], + script_hash: str, + wallets: List[str], + passwords: List[str], + address: str, + endpoint: str, + invoke_tx_file: str, + ) -> None: + keys = [f"bytes:{k}" for k in pubkeys] + keys_str = " ".join(keys) + multisig = Multisig( + self.neogo, invoke_tx_file=invoke_tx_file, block_period=self.block_period + ) + multisig.create_and_send( + self.designate_contract, + f"designateAsRole int:32 [ {keys_str} ]", + script_hash, + wallets, + passwords, + address, + endpoint, + ) + sleep(self.block_period) + + def set_inner_ring_multisig_tx( + self, + pubkeys: List[str], + script_hash: str, + wallets: List[str], + passwords: List[str], + address: str, + endpoint: str, + invoke_tx_file: str, + ) -> None: + keys = [f"bytes:{k}" for k in pubkeys] + keys_str = " ".join(keys) + multisig = Multisig( + self.neogo, invoke_tx_file=invoke_tx_file, block_period=self.block_period + ) + multisig.create_and_send( + self.designate_contract, + f"designateAsRole int:16 [ {keys_str} ]", + script_hash, + wallets, + passwords, + address, + endpoint, + ) + sleep(self.block_period) + + def check_candidates(self, contract_hash: str, endpoint: str) -> Optional[List[str]]: + out = self.neogo.contract.testinvokefunction( + scripthash=contract_hash, + method="innerRingCandidates", + rpc_endpoint=endpoint, + ) + output_dict = json.loads(out.stdout.replace("\n", "")) + candidates = output_dict["stack"][0]["value"] + if len(candidates) == 0: + return None + # TODO: return a list of keys + return [process_b64_bytearray(candidate["value"][0]["value"]) for candidate in candidates] diff --git a/src/neofs_testlib/blockchain/rpc_client.py b/src/neofs_testlib/blockchain/rpc_client.py new file mode 100644 index 00000000..b4e85c17 --- /dev/null +++ b/src/neofs_testlib/blockchain/rpc_client.py @@ -0,0 +1,80 @@ +import json +import logging +from typing import Any, Dict, List, Optional + +import requests + +logger = logging.getLogger("neofs.testlib.blockchain") + + +class NeoRPCException(Exception): + pass + + +class RPCClient: + def __init__(self, endpoint, timeout: int = 10): + self.endpoint = endpoint + self.timeout = timeout + + def get_raw_transaction(self, tx_hash): + return self._call_endpoint("getrawtransaction", params=[tx_hash]) + + def send_raw_transaction(self, raw_tx: str): + return self._call_endpoint("sendrawtransaction", params=[raw_tx]) + + def get_storage(self, sc_hash: str, storage_key: str): + return self._call_endpoint("getstorage", params=[sc_hash, storage_key]) + + def invoke_function( + self, + sc_hash: str, + function: str, + params: Optional[List] = None, + signers: Optional[List] = None, + ) -> Dict[str, Any]: + return self._call_endpoint( + "invokefunction", params=[sc_hash, function, params or [], signers or []] + ) + + def get_transaction_height(self, txid: str): + return self._call_endpoint("gettransactionheight", params=[txid]) + + def get_nep17_transfers(self, address, timestamps=None): + params = [address] + if timestamps: + params.append(timestamps) + return self._call_endpoint("getnep17transfers", params) + + def get_nep17_balances(self, address): + return self._call_endpoint("getnep17balances", [address, 0]) + + def get_application_log(self, tx_hash): + return self._call_endpoint("getapplicationlog", params=[tx_hash]) + + def get_contract_state(self, contract_id): + """ + `contract_id` might be contract name, script hash or number + """ + return self._call_endpoint("getcontractstate", params=[contract_id]) + + def _call_endpoint(self, method, params=None) -> Dict[str, Any]: + payload = _build_payload(method, params) + logger.info(payload) + try: + response = requests.post(self.endpoint, data=payload, timeout=self.timeout) + response.raise_for_status() + if response.status_code == 200: + if "result" in response.json(): + return response.json()["result"] + return response.json() + except Exception as exc: + raise NeoRPCException( + f"Could not call method {method} " + f"with endpoint: {self.endpoint}: {exc}" + f"\nRequest sent: {payload}" + ) from exc + + +def _build_payload(method, params: Optional[List] = None): + payload = json.dumps({"jsonrpc": "2.0", "method": method, "params": params or [], "id": 1}) + return payload.replace("'", '"') diff --git a/src/neofs_testlib/cli/cli_command.py b/src/neofs_testlib/cli/cli_command.py index 13268f26..772b9da1 100644 --- a/src/neofs_testlib/cli/cli_command.py +++ b/src/neofs_testlib/cli/cli_command.py @@ -1,11 +1,12 @@ from typing import Optional -from neofs_testlib.shell import CommandResult, Shell +from neofs_testlib.shell import CommandOptions, CommandResult, InteractiveInput, Shell class CliCommand: WALLET_SOURCE_ERROR_MSG = "Provide either wallet or wallet_config to specify wallet location" + WALLET_PASSWD_ERROR_MSG = "Provide either wallet_password or wallet_config to specify password" cli_exec_path: Optional[str] = None __base_params: Optional[str] = None @@ -14,6 +15,8 @@ class CliCommand: "await_mode": "await", "hash_type": "hash", "doc_type": "type", + "to_address": "to", + "from_address": "from", } def __init__(self, shell: Shell, cli_exec_path: str, **base_params): @@ -26,6 +29,9 @@ class CliCommand: def _format_command(self, command: str, **params) -> str: param_str = [] for param, value in params.items(): + if param == "post_data": + param_str.append(value) + continue if param in self.map_params.keys(): param = self.map_params[param] param = param.replace("_", "-") @@ -56,3 +62,11 @@ class CliCommand: def _execute(self, command: Optional[str], **params) -> CommandResult: return self.shell.exec(self._format_command(command, **params)) + + def _execute_with_password(self, command: Optional[str], password, **params) -> CommandResult: + return self.shell.exec( + self._format_command(command, **params), + options=CommandOptions( + interactive_inputs=[InteractiveInput(prompt_pattern="assword", input=password)] + ), + ) diff --git a/src/neofs_testlib/cli/neogo/candidate.py b/src/neofs_testlib/cli/neogo/candidate.py index 50200bbe..f5e4f338 100644 --- a/src/neofs_testlib/cli/neogo/candidate.py +++ b/src/neofs_testlib/cli/neogo/candidate.py @@ -11,6 +11,7 @@ class NeoGoCandidate(CliCommand): rpc_endpoint: str, wallet: Optional[str] = None, wallet_config: Optional[str] = None, + wallet_password: Optional[str] = None, gas: Optional[float] = None, timeout: int = 10, ) -> CommandResult: @@ -21,6 +22,7 @@ class NeoGoCandidate(CliCommand): wallet: Target location of the wallet file ('-' to read from stdin); conflicts with --wallet-config flag. wallet_config: Target location of the wallet config file; conflicts with --wallet flag. + wallet_password: Wallet password. gas: Network fee to add to the transaction (prioritizing it). rpc_endpoint: RPC node address. timeout: Timeout for the operation (default: 10s). @@ -29,15 +31,20 @@ class NeoGoCandidate(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG + exec_param = { + param: param_value + for param, param_value in locals().items() + if param not in ["self", "wallet_password"] + } + exec_param["timeout"] = f"{timeout}s" + if wallet_password is not None: + return self._execute_with_password( + "wallet candidate register", wallet_password, **exec_param + ) + if wallet_config: + return self._execute("wallet candidate register", **exec_param) - return self._execute( - "wallet candidate register", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, - ) + raise Exception(self.WALLET_PASSWD_ERROR_MSG) def unregister( self, @@ -45,6 +52,7 @@ class NeoGoCandidate(CliCommand): rpc_endpoint: str, wallet: Optional[str] = None, wallet_config: Optional[str] = None, + wallet_password: Optional[str] = None, gas: Optional[float] = None, timeout: int = 10, ) -> CommandResult: @@ -55,6 +63,7 @@ class NeoGoCandidate(CliCommand): wallet: Target location of the wallet file ('-' to read from stdin); conflicts with --wallet-config flag. wallet_config: Target location of the wallet config file; conflicts with --wallet flag. + wallet_password: Wallet password. gas: Network fee to add to the transaction (prioritizing it). rpc_endpoint: RPC node address. timeout: Timeout for the operation (default: 10s). @@ -63,22 +72,29 @@ class NeoGoCandidate(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG + exec_param = { + param: param_value + for param, param_value in locals().items() + if param not in ["self", "wallet_password"] + } + exec_param["timeout"] = f"{timeout}s" + if wallet_password is not None: + return self._execute_with_password( + "wallet candidate unregister", wallet_password, **exec_param + ) + if wallet_config: + return self._execute("wallet candidate unregister", **exec_param) - return self._execute( - "wallet candidate unregister", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, - ) + raise Exception(self.WALLET_PASSWD_ERROR_MSG) def vote( self, + address: str, candidate: str, rpc_endpoint: str, wallet: Optional[str] = None, wallet_config: Optional[str] = None, + wallet_password: Optional[str] = None, gas: Optional[float] = None, timeout: int = 10, ) -> CommandResult: @@ -88,10 +104,12 @@ class NeoGoCandidate(CliCommand): candidate argument to perform unvoting. Args: + address: Address to vote from candidate: Public key of candidate to vote for. wallet: Target location of the wallet file ('-' to read from stdin); conflicts with --wallet-config flag. wallet_config: Target location of the wallet config file; conflicts with --wallet flag. + wallet_password: Wallet password. gas: Network fee to add to the transaction (prioritizing it). rpc_endpoint: RPC node address. timeout: Timeout for the operation (default: 10s). @@ -100,12 +118,17 @@ class NeoGoCandidate(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG + exec_param = { + param: param_value + for param, param_value in locals().items() + if param not in ["self", "wallet_password"] + } + exec_param["timeout"] = f"{timeout}s" + if wallet_password is not None: + return self._execute_with_password( + "wallet candidate vote", wallet_password, **exec_param + ) + if wallet_config: + return self._execute("wallet candidate vote", **exec_param) - return self._execute( - "wallet candidate vote", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, - ) + raise Exception(self.WALLET_PASSWD_ERROR_MSG) diff --git a/src/neofs_testlib/cli/neogo/contract.py b/src/neofs_testlib/cli/neogo/contract.py index 53299789..61f4edb1 100644 --- a/src/neofs_testlib/cli/neogo/contract.py +++ b/src/neofs_testlib/cli/neogo/contract.py @@ -45,11 +45,12 @@ class NeoGoContract(CliCommand): self, address: str, input_file: str, - sysgas: float, manifest: str, rpc_endpoint: str, + sysgas: Optional[float] = None, wallet: Optional[str] = None, wallet_config: Optional[str] = None, + wallet_password: Optional[str] = None, gas: Optional[float] = None, out: Optional[str] = None, force: bool = False, @@ -62,6 +63,7 @@ class NeoGoContract(CliCommand): conflicts with wallet_config. wallet_config: Path to wallet config to use to get the key for transaction signing; conflicts with wallet. + wallet_password: Wallet password. address: Address to use as transaction signee (and gas source). gas: Network fee to add to the transaction (prioritizing it). sysgas: System fee to add to transaction (compensating for execution). @@ -77,15 +79,26 @@ class NeoGoContract(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG + exec_param = { + param: param_value + for param, param_value in locals().items() + if param not in ["self", "wallet_password"] + } + exec_param["timeout"] = f"{timeout}s" - return self._execute( - "contract deploy", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, - ) + if wallet_password is not None: + return self._execute_with_password( + "contract deploy", + wallet_password, + **exec_param, + ) + if wallet_config: + return self._execute( + "contract deploy", + **exec_param, + ) + + raise Exception(self.WALLET_PASSWD_ERROR_MSG) def generate_wrapper( self, @@ -116,13 +129,14 @@ class NeoGoContract(CliCommand): def invokefunction( self, - address: str, scripthash: str, + address: Optional[str] = None, wallet: Optional[str] = None, method: Optional[str] = None, arguments: Optional[str] = None, multisig_hash: Optional[str] = None, wallet_config: Optional[str] = None, + wallet_password: Optional[str] = None, gas: Optional[float] = None, sysgas: Optional[float] = None, out: Optional[str] = None, @@ -147,6 +161,7 @@ class NeoGoContract(CliCommand): conflicts with wallet_config. wallet_config: Path to wallet config to use to get the key for transaction signing; conflicts with wallet. + wallet_password: Wallet password. address: Address to use as transaction signee (and gas source). gas: Network fee to add to the transaction (prioritizing it). sysgas: System fee to add to transaction (compensating for execution). @@ -158,21 +173,40 @@ class NeoGoContract(CliCommand): Returns: Command's result. """ + + assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG + multisig_hash = f"-- {multisig_hash}" or "" - return self._execute( - "contract invokefunction " - f"{scripthash} {method or ''} {arguments or ''} {multisig_hash}", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self", "scripthash", "method", "arguments", "multisig_hash"] - }, - ) + post_data = f"{scripthash} {method or ''} {arguments or ''} {multisig_hash}" + exec_param = { + param: param_value + for param, param_value in locals().items() + if param + not in [ + "self", + "scripthash", + "method", + "arguments", + "multisig_hash", + "wallet_password", + ] + } + exec_param["timeout"] = f"{timeout}s" + exec_param["post_data"] = post_data + if wallet_password is not None: + return self._execute_with_password( + "contract invokefunction", wallet_password, **exec_param + ) + if wallet_config: + return self._execute("contract invokefunction", **exec_param) + + raise Exception(self.WALLET_PASSWD_ERROR_MSG) def testinvokefunction( self, scripthash: str, wallet: Optional[str] = None, + wallet_password: Optional[str] = None, method: Optional[str] = None, arguments: Optional[str] = None, multisig_hash: Optional[str] = None, @@ -192,6 +226,8 @@ class NeoGoContract(CliCommand): Args: scripthash: Function hash. + wallet: Wallet to use for testinvoke. + wallet_password: Wallet password. method: Call method. arguments: Method arguments. multisig_hash: Multisig hash. @@ -201,16 +237,29 @@ class NeoGoContract(CliCommand): Returns: Command's result. """ - multisig_hash = f"-- {multisig_hash}" or "" - return self._execute( - "contract testinvokefunction " - f"{scripthash} {method or ''} {arguments or ''} {multisig_hash}", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self", "scripthash", "method", "arguments", "multisig_hash"] - }, - ) + multisig_hash = f"-- {multisig_hash}" if multisig_hash else "" + post_data = f"{scripthash} {method or ''} {arguments or ''} {multisig_hash}" + exec_param = { + param: param_value + for param, param_value in locals().items() + if param + not in [ + "self", + "scripthash", + "method", + "arguments", + "multisig_hash", + "wallet_password", + ] + } + exec_param["timeout"] = f"{timeout}s" + exec_param["post_data"] = post_data + if wallet_password is not None: + return self._execute_with_password( + "contract testinvokefunction", wallet_password, **exec_param + ) + + return self._execute("contract testinvokefunction", **exec_param) def testinvokescript( self, @@ -231,13 +280,13 @@ class NeoGoContract(CliCommand): Returns: Command's result. """ + exec_param = { + param: param_value for param, param_value in locals().items() if param not in ["self"] + } + exec_param["timeout"] = f"{timeout}s" return self._execute( - f"contract testinvokescript", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, + "contract testinvokescript", + **exec_param, ) def init(self, name: str, skip_details: bool = False) -> CommandResult: @@ -313,14 +362,18 @@ class NeoGoContract(CliCommand): address: str, wallet: Optional[str] = None, wallet_config: Optional[str] = None, + wallet_password: Optional[str] = None, sender: Optional[str] = None, nef: Optional[str] = None, ) -> CommandResult: """Adds group to the manifest. Args: - wallet: Wallet to use to get the key for transaction signing; conflicts with wallet_config. - wallet_config: Path to wallet config to use to get the key for transaction signing; conflicts with wallet. + wallet: Wallet to use to get the key for transaction signing; + conflicts with wallet_config. + wallet_config: Path to wallet config to use to get the key for transaction signing; + conflicts with wallet. + wallet_password: Wallet password. sender: Deploy transaction sender. address: Account to sign group with. nef: Path to the NEF file. @@ -329,11 +382,17 @@ class NeoGoContract(CliCommand): Returns: Command's result. """ - return self._execute( - "contract manifest add-group", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, - ) + assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG + exec_param = { + param: param_value + for param, param_value in locals().items() + if param not in ["self", "wallet_password"] + } + if wallet_password is not None: + return self._execute_with_password( + "contract manifest add-group", wallet_password, **exec_param + ) + if wallet_config: + return self._execute("contract manifest add-group", **exec_param) + + raise Exception(self.WALLET_PASSWD_ERROR_MSG) diff --git a/src/neofs_testlib/cli/neogo/go.py b/src/neofs_testlib/cli/neogo/go.py index 02aac736..5f216ce9 100644 --- a/src/neofs_testlib/cli/neogo/go.py +++ b/src/neofs_testlib/cli/neogo/go.py @@ -24,7 +24,7 @@ class NeoGo: def __init__( self, shell: Shell, - neo_go_exec_path: Optional[str] = None, + neo_go_exec_path: str, config_path: Optional[str] = None, ): self.candidate = NeoGoCandidate(shell, neo_go_exec_path, config_path=config_path) diff --git a/src/neofs_testlib/cli/neogo/nep17.py b/src/neofs_testlib/cli/neogo/nep17.py index edd65ebc..7cc00b65 100644 --- a/src/neofs_testlib/cli/neogo/nep17.py +++ b/src/neofs_testlib/cli/neogo/nep17.py @@ -29,14 +29,13 @@ class NeoGoNep17(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG - + exec_param = { + param: param_value for param, param_value in locals().items() if param not in ["self"] + } + exec_param["timeout"] = f"{timeout}s" return self._execute( "wallet nep17 balance", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, + **exec_param, ) def import_token( @@ -63,14 +62,13 @@ class NeoGoNep17(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG - + exec_param = { + param: param_value for param, param_value in locals().items() if param not in ["self"] + } + exec_param["timeout"] = f"{timeout}s" return self._execute( "wallet nep17 import", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, + **exec_param, ) def info( @@ -133,10 +131,11 @@ class NeoGoNep17(CliCommand): self, token: str, to_address: str, - sysgas: float, rpc_endpoint: str, + sysgas: Optional[float] = None, wallet: Optional[str] = None, wallet_config: Optional[str] = None, + wallet_password: Optional[str] = None, out: Optional[str] = None, from_address: Optional[str] = None, force: bool = False, @@ -156,6 +155,7 @@ class NeoGoNep17(CliCommand): wallet: Target location of the wallet file ('-' to read from stdin); conflicts with --wallet-config flag. wallet_config: Target location of the wallet config file; conflicts with --wallet flag. + wallet_password: Wallet password. out: File to put JSON transaction to. from_address: Address to send an asset from. to_address: Address to send an asset to. @@ -172,15 +172,26 @@ class NeoGoNep17(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG + exec_param = { + param: param_value + for param, param_value in locals().items() + if param not in ["self", "wallet_password"] + } + exec_param["timeout"] = f"{timeout}s" - return self._execute( - "wallet nep17 transfer", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, - ) + if wallet_password is not None: + return self._execute_with_password( + "wallet nep17 transfer", + wallet_password, + **exec_param, + ) + if wallet_config: + return self._execute( + "wallet nep17 transfer", + **exec_param, + ) + + raise Exception(self.WALLET_PASSWD_ERROR_MSG) def multitransfer( self, @@ -219,12 +230,11 @@ class NeoGoNep17(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG - + exec_param = { + param: param_value for param, param_value in locals().items() if param not in ["self"] + } + exec_param["timeout"] = f"{timeout}s" return self._execute( "wallet nep17 multitransfer", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, + **exec_param, ) diff --git a/src/neofs_testlib/cli/neogo/query.py b/src/neofs_testlib/cli/neogo/query.py index 6d937991..945cd6cd 100644 --- a/src/neofs_testlib/cli/neogo/query.py +++ b/src/neofs_testlib/cli/neogo/query.py @@ -3,7 +3,7 @@ from neofs_testlib.shell import CommandResult class NeoGoQuery(CliCommand): - def candidates(self, rpc_endpoint: str, timeout: int = 10) -> CommandResult: + def candidates(self, rpc_endpoint: str, timeout: str = "10s") -> CommandResult: """Get candidates and votes. Args: @@ -22,7 +22,7 @@ class NeoGoQuery(CliCommand): }, ) - def committee(self, rpc_endpoint: str, timeout: int = 10) -> CommandResult: + def committee(self, rpc_endpoint: str, timeout: str = "10s") -> CommandResult: """Get committee list. Args: @@ -41,7 +41,7 @@ class NeoGoQuery(CliCommand): }, ) - def height(self, rpc_endpoint: str, timeout: int = 10) -> CommandResult: + def height(self, rpc_endpoint: str, timeout: str = "10s") -> CommandResult: """Get node height. Args: @@ -60,7 +60,7 @@ class NeoGoQuery(CliCommand): }, ) - def tx(self, tx_hash: str, rpc_endpoint: str, timeout: int = 10) -> CommandResult: + def tx(self, tx_hash: str, rpc_endpoint: str, timeout: str = "10s") -> CommandResult: """Query transaction status. Args: @@ -80,7 +80,7 @@ class NeoGoQuery(CliCommand): }, ) - def voter(self, rpc_endpoint: str, timeout: int = 10) -> CommandResult: + def voter(self, rpc_endpoint: str, timeout: str = "10s") -> CommandResult: """Print NEO holder account state. Args: diff --git a/src/neofs_testlib/cli/neogo/wallet.py b/src/neofs_testlib/cli/neogo/wallet.py index e327fb5b..9e95a510 100644 --- a/src/neofs_testlib/cli/neogo/wallet.py +++ b/src/neofs_testlib/cli/neogo/wallet.py @@ -27,14 +27,13 @@ class NeoGoWallet(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG - + exec_param = { + param: param_value for param, param_value in locals().items() if param not in ["self"] + } + exec_param["timeout"] = f"{timeout}s" return self._execute( "wallet claim", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, + **exec_param, ) def init( @@ -293,14 +292,13 @@ class NeoGoWallet(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG - + exec_param = { + param: param_value for param, param_value in locals().items() if param not in ["self"] + } + exec_param["timeout"] = f"{timeout}s" return self._execute( "wallet import-deployed", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, + **exec_param, ) def remove( @@ -337,9 +335,10 @@ class NeoGoWallet(CliCommand): self, input_file: str, address: str, - rpc_endpoint: str, + rpc_endpoint: Optional[str] = None, wallet: Optional[str] = None, wallet_config: Optional[str] = None, + wallet_password: Optional[str] = None, out: Optional[str] = None, timeout: int = 10, ) -> CommandResult: @@ -356,6 +355,7 @@ class NeoGoWallet(CliCommand): wallet: Target location of the wallet file ('-' to read from stdin); conflicts with --wallet-config flag. wallet_config: Target location of the wallet config file; conflicts with --wallet flag. + wallet_password: Wallet password. out: File to put JSON transaction to. input_file: File with JSON transaction. address: Address to use. @@ -366,12 +366,16 @@ class NeoGoWallet(CliCommand): Command's result. """ assert bool(wallet) ^ bool(wallet_config), self.WALLET_SOURCE_ERROR_MSG + exec_param = { + param: param_value + for param, param_value in locals().items() + if param not in ["self", "wallet_password"] + } + exec_param["timeout"] = f"{timeout}s" + if wallet_password is not None: + return self._execute_with_password("wallet sign", wallet_password, **exec_param) - return self._execute( - "wallet sign", - **{ - param: param_value - for param, param_value in locals().items() - if param not in ["self"] - }, - ) + if wallet_config: + return self._execute("wallet sign", **exec_param) + + raise Exception(self.WALLET_PASSWD_ERROR_MSG) diff --git a/src/neofs_testlib/shell/__init__.py b/src/neofs_testlib/shell/__init__.py index 3fd63bdc..d0f22d6b 100644 --- a/src/neofs_testlib/shell/__init__.py +++ b/src/neofs_testlib/shell/__init__.py @@ -1,3 +1,3 @@ -from neofs_testlib.shell.interfaces import CommandOptions, CommandResult, Shell +from neofs_testlib.shell.interfaces import CommandOptions, CommandResult, InteractiveInput, Shell from neofs_testlib.shell.local_shell import LocalShell from neofs_testlib.shell.ssh_shell import SSHShell diff --git a/src/neofs_testlib/utils/__init__.py b/src/neofs_testlib/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/src/neofs_testlib/utils/converters.py b/src/neofs_testlib/utils/converters.py new file mode 100644 index 00000000..64cef1a6 --- /dev/null +++ b/src/neofs_testlib/utils/converters.py @@ -0,0 +1,69 @@ +import base64 +import binascii +import json + +import base58 +from neo3 import wallet as neo3_wallet + + +def str_to_ascii_hex(input: str) -> str: + b = binascii.hexlify(input.encode()) + return str(b)[2:-1] + + +def ascii_hex_to_str(input: str) -> bytes: + return bytes.fromhex(input) + + +# Two functions below do parsing of Base64-encoded byte arrays which +# tests receive from Neo node RPC calls. + + +def process_b64_bytearray_reverse(data: str) -> bytes: + """ + This function decodes input data from base64, reverses the byte + array and returns its string representation. + """ + arr = bytearray(base64.standard_b64decode(data)) + arr.reverse() + return binascii.b2a_hex(arr) + + +def process_b64_bytearray(data: str) -> bytes: + """ + This function decodes input data from base64 and returns the + bytearray string representation. + """ + arr = bytearray(base64.standard_b64decode(data)) + return binascii.b2a_hex(arr) + + +def contract_hash_to_address(chash: str) -> str: + """ + This function accepts contract hash in BE, then translates in to LE, + prepends NEO wallet prefix and encodes to base58. It is equal to + `UInt160ToString` method in NEO implementations. + """ + be = bytearray(bytes.fromhex(chash)) + be.reverse() + return base58.b58encode_check(b"\x35" + bytes(be)).decode() + + +def get_contract_hash_from_manifest(manifest_path: str) -> str: + with open(manifest_path) as m: + data = json.load(m) + # cut off '0x' and return the hash + return data["abi"]["hash"][2:] + + +def get_wif_from_private_key(priv_key: bytes) -> str: + wif_version = b"\x80" + compressed_flag = b"\x01" + wif = base58.b58encode_check(wif_version + priv_key + compressed_flag) + return wif.decode("utf-8") + + +def load_wallet(path: str, passwd: str = "") -> neo3_wallet.Wallet: + with open(path, "r") as wallet_file: + wlt_data = wallet_file.read() + return neo3_wallet.Wallet.from_json(json.loads(wlt_data), password=passwd) diff --git a/src/neofs_testlib/utils/wallet.py b/src/neofs_testlib/utils/wallet.py new file mode 100644 index 00000000..656c41fa --- /dev/null +++ b/src/neofs_testlib/utils/wallet.py @@ -0,0 +1,37 @@ +import json +import logging + +from neo3 import wallet as neo3_wallet + +logger = logging.getLogger("neofs.testlib.utils") + + +def init_wallet(wallet_path: str, wallet_password: str): + """ + Create new wallet and new account. + Args: + wallet_path: The path to the wallet to save wallet. + wallet_password: The password for new wallet. + """ + wallet = neo3_wallet.Wallet() + account = neo3_wallet.Account.create_new(wallet_password) + wallet.account_add(account) + with open(wallet_path, "w") as out: + json.dump(wallet.to_json(), out) + logger.info(f"Init new wallet: {wallet_path}, address: {account.address}") + + +def get_last_address_from_wallet(wallet_path: str, wallet_password: str): + """ + Extracting the last address from the given wallet. + Args: + wallet_path: The path to the wallet to extract address from. + wallet_password: The password for the given wallet. + Returns: + The address for the wallet. + """ + with open(wallet_path) as wallet_file: + wallet = neo3_wallet.Wallet.from_json(json.load(wallet_file), password=wallet_password) + address = wallet.accounts[-1].address + logger.info(f"got address: {address}") + return address \ No newline at end of file