Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added suport for contract queries using abi file #436

Merged
merged 11 commits into from
Jul 30, 2024
20 changes: 16 additions & 4 deletions multiversx_sdk_cli/cli_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
from multiversx_sdk_cli.constants import NUMBER_OF_SHARDS
from multiversx_sdk_cli.contract_verification import \
trigger_contract_verification
from multiversx_sdk_cli.contracts import SmartContract, query_contract
from multiversx_sdk_cli.contracts import SmartContract
from multiversx_sdk_cli.cosign_transaction import cosign_transaction
from multiversx_sdk_cli.dependency_checker import check_if_rust_is_installed
from multiversx_sdk_cli.docker import is_docker_installed, run_docker
Expand Down Expand Up @@ -132,6 +132,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any:
sub = cli_shared.add_command_subparser(subparsers, "contract", "query",
"Query a Smart Contract (call a pure function)")
_add_contract_arg(sub)
_add_contract_abi_arg(sub)
cli_shared.add_proxy_arg(sub)
_add_function_arg(sub)
_add_arguments_arg(sub)
Expand Down Expand Up @@ -443,17 +444,28 @@ def upgrade(args: Any):
def query(args: Any):
logger.debug("query")

# workaround so we can use the function bellow
# workaround so we can use the function bellow to set chainID
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo below.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

args.chain = ""
cli_shared.prepare_chain_id_in_args(args)

config = TransactionsFactoryConfig(args.chain)
abi = Abi.load(Path(args.abi)) if args.abi else None
contract = SmartContract(config, abi)

arguments, should_prepare_args = _get_contract_arguments(args)
contract_address = Address.new_from_bech32(args.contract)

proxy = ProxyNetworkProvider(args.proxy)
function = args.function
arguments: List[Any] = args.arguments or []

result = query_contract(contract_address, proxy, function, arguments)
result = contract.query_contract(
contract_address=contract_address,
proxy=proxy,
function=function,
arguments=arguments,
should_prepare_args=should_prepare_args
)

utils.dump_out_json(result)


Expand Down
18 changes: 15 additions & 3 deletions multiversx_sdk_cli/cli_dns.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
dns_address_for_name, name_hash, register,
registration_cost, resolve, validate_name,
version)
from multiversx_sdk_cli.errors import ArgumentsNotProvidedError


def setup_parser(args: List[str], subparsers: Any) -> Any:
Expand All @@ -32,7 +33,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any:

sub = cli_shared.add_command_subparser(subparsers, "dns", "validate-name", "Asks one of the DNS contracts to validate a name. Can be useful before registering it.")
_add_name_arg(sub)
sub.add_argument("--shard-id", default=0, help="shard id of the contract to call (default: %(default)s)")
sub.add_argument("--shard-id", type=int, default=0, help="shard id of the contract to call (default: %(default)s)")
cli_shared.add_proxy_arg(sub)
sub.set_defaults(func=dns_validate_name)

Expand All @@ -41,12 +42,12 @@ def setup_parser(args: List[str], subparsers: Any) -> Any:
sub.set_defaults(func=get_name_hash)

sub = cli_shared.add_command_subparser(subparsers, "dns", "registration-cost", "Gets the registration cost from a DNS smart contract, by default the one with shard id 0.")
sub.add_argument("--shard-id", default=0, help="shard id of the contract to call (default: %(default)s)")
sub.add_argument("--shard-id", type=int, default=0, help="shard id of the contract to call (default: %(default)s)")
cli_shared.add_proxy_arg(sub)
sub.set_defaults(func=get_registration_cost)

sub = cli_shared.add_command_subparser(subparsers, "dns", "version", "Asks the contract for its version")
sub.add_argument("--shard-id", default=0, help="shard id of the contract to call (default: %(default)s)")
sub.add_argument("--shard-id", type=int, default=0, help="shard id of the contract to call (default: %(default)s)")
sub.add_argument("--all", action="store_true", default=False, help="prints a list of all DNS contracts and their current versions (default: %(default)s)")
cli_shared.add_proxy_arg(sub)
sub.set_defaults(func=get_version)
Expand All @@ -70,13 +71,21 @@ def _add_name_arg(sub: Any):
sub.add_argument("name", help="the name for which to check")


def _ensure_proxy_is_provided(args: Any):
if not args.proxy:
raise ArgumentsNotProvidedError("'--proxy' argument not provided")


def dns_resolve(args: Any):
_ensure_proxy_is_provided(args)

addr = resolve(args.name, ProxyNetworkProvider(args.proxy))
if addr.to_hex() != Address.new_from_bech32(ADDRESS_ZERO_BECH32).to_hex():
print(addr.to_bech32())


def dns_validate_name(args: Any):
_ensure_proxy_is_provided(args)
validate_name(args.name, args.shard_id, ProxyNetworkProvider(args.proxy))


Expand All @@ -97,10 +106,13 @@ def get_dns_address_for_name_hex(args: Any):


def get_registration_cost(args: Any):
_ensure_proxy_is_provided(args)
print(registration_cost(args.shard_id, ProxyNetworkProvider(args.proxy)))


def get_version(args: Any):
_ensure_proxy_is_provided(args)

proxy = ProxyNetworkProvider(args.proxy)
if args.all:
t = PrettyTable(['Shard ID', 'Contract address (bech32)', 'Contract address (hex)', 'Version'])
Expand Down
5 changes: 4 additions & 1 deletion multiversx_sdk_cli/cli_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
import sys
from pathlib import Path
from typing import Any, List, Optional, Tuple
from typing import Any, List, Optional, Tuple, cast

from multiversx_sdk import (Address, Mnemonic, UserPEM, UserSecretKey,
UserWallet)
Expand Down Expand Up @@ -134,6 +134,9 @@ def wallet_new(args: Any):
else:
mnemonic = Mnemonic.generate()

# this is done to get rid of the Pylance error: possibly unbound
mnemonic = cast(Mnemonic, mnemonic) # type: ignore
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can also fix the warning by extracting the above logic to a separate function e.g. generate_mnemonic_with_shard_constraint() or something like that.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indeed, extracted to a separate function.


print(f"Mnemonic: {mnemonic.get_text()}")
print(f"Wallet address: {mnemonic.derive_key().generate_public_key().to_address(address_hrp).to_bech32()}")

Expand Down
128 changes: 54 additions & 74 deletions multiversx_sdk_cli/contracts.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import base64
import logging
from pathlib import Path
from typing import Any, List, Optional, Protocol, Sequence, Union
from typing import Any, Dict, List, Optional, Protocol, Sequence, Union

from multiversx_sdk import (Address, SmartContractTransactionsFactory, Token,
from multiversx_sdk import (Address, QueryRunnerAdapter,
SmartContractQueriesController,
SmartContractQueryResponse,
SmartContractTransactionsFactory, Token,
TokenComputer, TokenTransfer, Transaction,
TransactionPayload)
from multiversx_sdk.abi import Abi
Expand Down Expand Up @@ -63,6 +65,10 @@
return_data: List[str]
return_code: str
return_message: str
gas_used: int
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we reference the latest & greatest feat/next of sdk-py, maybe we can drop this interface (or maybe not yet)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can drop it.


def get_return_data_parts(self) -> List[bytes]:
...


class IConfig(Protocol):
Expand Down Expand Up @@ -95,7 +101,7 @@
guardian: str) -> Transaction:
args = arguments if arguments else []
if should_prepare_args:
args = self.prepare_args_for_factory(args)
args = self._prepare_args_for_factory(args)

tx = self._factory.create_transaction_for_deploy(
sender=owner.address,
Expand Down Expand Up @@ -133,7 +139,7 @@

args = arguments if arguments else []
if should_prepare_args:
args = self.prepare_args_for_factory(args)
args = self._prepare_args_for_factory(args)

tx = self._factory.create_transaction_for_execute(
sender=caller.address,
Expand Down Expand Up @@ -170,7 +176,7 @@
guardian: str) -> Transaction:
args = arguments if arguments else []
if should_prepare_args:
args = self.prepare_args_for_factory(args)
args = self._prepare_args_for_factory(args)

tx = self._factory.create_transaction_for_upgrade(
sender=owner.address,
Expand All @@ -192,6 +198,40 @@

return tx

def query_contract(self,

Check warning on line 201 in multiversx_sdk_cli/contracts.py

View workflow job for this annotation

GitHub Actions / runner / mypy

[mypy] reported by reviewdog 🐶 "query_contract" of "SmartContract" defined here Raw Output: /home/runner/work/mx-sdk-py-cli/mx-sdk-py-cli/multiversx_sdk_cli/contracts.py:201:5: note: "query_contract" of "SmartContract" defined here

Check warning on line 201 in multiversx_sdk_cli/contracts.py

View workflow job for this annotation

GitHub Actions / runner / mypy

[mypy] reported by reviewdog 🐶 "query_contract" of "SmartContract" defined here Raw Output: /home/runner/work/mx-sdk-py-cli/mx-sdk-py-cli/multiversx_sdk_cli/contracts.py:201:5: note: "query_contract" of "SmartContract" defined here

Check warning on line 201 in multiversx_sdk_cli/contracts.py

View workflow job for this annotation

GitHub Actions / runner / mypy

[mypy] reported by reviewdog 🐶 "query_contract" of "SmartContract" defined here Raw Output: /home/runner/work/mx-sdk-py-cli/mx-sdk-py-cli/multiversx_sdk_cli/contracts.py:201:5: note: "query_contract" of "SmartContract" defined here

Check warning on line 201 in multiversx_sdk_cli/contracts.py

View workflow job for this annotation

GitHub Actions / runner / mypy

[mypy] reported by reviewdog 🐶 "query_contract" of "SmartContract" defined here Raw Output: /home/runner/work/mx-sdk-py-cli/mx-sdk-py-cli/multiversx_sdk_cli/contracts.py:201:5: note: "query_contract" of "SmartContract" defined here
contract_address: IAddress,
proxy: INetworkProvider,
function: str,
arguments: List[Any],
should_prepare_args: bool) -> List[Any]:
args = arguments if arguments else []
if should_prepare_args:
args = self._prepare_args_for_factory(args)

query_runner = QueryRunnerAdapter(proxy)
sc_query_controller = SmartContractQueriesController(query_runner, self._abi)

query = sc_query_controller.create_query(
contract=contract_address.to_bech32(),
function=function,
arguments=args
)

response = sc_query_controller.run_query(query)

if self._abi:
Copy link

@axenteoctavian axenteoctavian Jul 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if-else is not consistent.
a)
With ABI you return the data-parts
Without ABI your return other stuff + data-parts as Dict

b) errors for bad queries
With ABI you raise SmartContractQueryError
Without ABI you return the Dict with error message

1/ What is the reason you return different types?
To make it consistent you can just return sc_query_controller.parse_query_response(response) but for NO abi case return parts as hex.
2/ Please add some unit tests for bad queries (wrong --function or whatever)
3/ I can't find test for contract query with abi

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now, the if-else is gone. The querying is done in a try-except block so all the errors thrown are of type
QueryContractError. Did a small "fix" for the json encoder to convert all bytes to hex. Also added tests.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me

return sc_query_controller.parse_query_response(response)
else:
return [self._query_response_to_dict(response)]

def _query_response_to_dict(self, response: SmartContractQueryResponse) -> Dict[str, Any]:
return {
"function": response.function,
"returnCode": response.return_code,
"returnMessage": response.return_message,
"returnDataParts": [part.hex() for part in response.return_data_parts]
}

def _prepare_token_transfers(self, transfers: List[str]) -> List[TokenTransfer]:
token_computer = TokenComputer()
token_transfers: List[TokenTransfer] = []
Expand All @@ -207,12 +247,12 @@

return token_transfers

def prepare_args_for_factory(self, arguments: List[str]) -> List[Any]:
def _prepare_args_for_factory(self, arguments: List[str]) -> List[Any]:
args: List[Any] = []

for arg in arguments:
if arg.startswith(HEX_PREFIX):
args.append(hex_to_bytes(arg))
args.append(self._hex_to_bytes(arg))
elif arg.isnumeric():
args.append(int(arg))
elif arg.startswith(DEFAULT_HRP):
Expand All @@ -228,64 +268,11 @@

return args


def query_contract(
contract_address: IAddress,
proxy: INetworkProvider,
function: str,
arguments: List[Any],
value: int = 0,
caller: Optional[IAddress] = None
) -> List[Any]:
response_data = query_detailed(contract_address, proxy, function, arguments, value, caller)
return_data = response_data.return_data
return [_interpret_return_data(data) for data in return_data]


def query_detailed(contract_address: IAddress, proxy: INetworkProvider, function: str, arguments: List[Any],
value: int = 0, caller: Optional[IAddress] = None) -> Any:
arguments = arguments or []
# Temporary workaround, until we use sdk-core's serializer.
arguments_hex = [_prepare_argument(arg) for arg in arguments]
prepared_arguments_bytes = [bytes.fromhex(arg) for arg in arguments_hex]

query = ContractQuery(contract_address, function, value, prepared_arguments_bytes, caller)

response = proxy.query_contract(query)
# Temporary workaround, until we add "isSuccess" on the response class.
if response.return_code != "ok":
raise RuntimeError(f"Query failed: {response.return_message}")
return response


def _interpret_return_data(data: str) -> Any:
if not data:
return data

try:
as_bytes = base64.b64decode(data)
as_hex = as_bytes.hex()
as_number = _interpret_as_number_if_safely(as_hex)

result = QueryResult(data, as_hex, as_number)
return result
except Exception:
logger.warn(f"Cannot interpret return data: {data}")
return None


def _interpret_as_number_if_safely(as_hex: str) -> Optional[int]:
"""
Makes sure the string can be safely converted to an int (and then back to a string).

See:
- https://stackoverflow.com/questions/73693104/valueerror-exceeds-the-limit-4300-for-integer-string-conversion
- https://github.com/python/cpython/issues/95778
"""
try:
return int(str(int(as_hex or "0", 16)))
except Exception:
return None
def _hex_to_bytes(self, arg: str):
argument = arg[len(HEX_PREFIX):]
argument = argument.upper()
argument = ensure_even_length(argument)
return bytes.fromhex(argument)


def prepare_execute_transaction_data(function: str, arguments: List[Any]) -> TransactionPayload:
Expand All @@ -297,14 +284,7 @@
return TransactionPayload.from_str(tx_data)


def hex_to_bytes(arg: str):
argument = arg[len(HEX_PREFIX):]
argument = argument.upper()
argument = ensure_even_length(argument)
return bytes.fromhex(argument)


# only used for contract queries and stake operations
# only used for stake operations
def _prepare_argument(argument: Any):
as_str = str(argument)
as_hex = _to_hex(as_str)
Expand Down
Loading
Loading