Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions multiversx_sdk_cli/args_converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import logging
from typing import Any

from multiversx_sdk import Address
from multiversx_sdk.abi import (
AddressValue,
BigUIntValue,
BoolValue,
BytesValue,
StringValue,
)

from multiversx_sdk_cli.config_env import get_address_hrp
from multiversx_sdk_cli.constants import (
ADDRESS_PREFIX,
FALSE_STR_LOWER,
HEX_PREFIX,
MAINCHAIN_ADDRESS_HRP,
STR_PREFIX,
TRUE_STR_LOWER,
)
from multiversx_sdk_cli.errors import BadUserInput

logger = logging.getLogger("args_converter")


def convert_args_to_typed_values(arguments: list[str]) -> list[Any]:
args: list[Any] = []

for arg in arguments:
if arg.startswith(HEX_PREFIX):
args.append(BytesValue(_hex_to_bytes(arg)))
elif arg.isnumeric():
args.append(BigUIntValue(int(arg)))
elif arg.startswith(ADDRESS_PREFIX):
args.append(AddressValue.new_from_address(Address.new_from_bech32(arg[len(ADDRESS_PREFIX) :])))
elif arg.startswith(MAINCHAIN_ADDRESS_HRP):
# this flow will be removed in the future
logger.warning(
"Address argument has no prefix. This flow will be removed in the future. Please provide each address using the `addr:` prefix. (e.g. --arguments addr:erd1...)"
)
args.append(AddressValue.new_from_address(Address.new_from_bech32(arg)))
elif arg.startswith(get_address_hrp()):
args.append(AddressValue.new_from_address(Address.new_from_bech32(arg)))
elif arg.lower() == FALSE_STR_LOWER:
args.append(BoolValue(False))
elif arg.lower() == TRUE_STR_LOWER:
args.append(BoolValue(True))
elif arg.startswith(STR_PREFIX):
args.append(StringValue(arg[len(STR_PREFIX) :]))
else:
raise BadUserInput(
f"Unknown argument type for argument: `{arg}`. Use `mxpy contract <sub-command> --help` to check all supported arguments"
)

return args


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


def _ensure_even_length(string: str) -> str:
if len(string) % 2 == 1:
return "0" + string
return string
188 changes: 114 additions & 74 deletions multiversx_sdk_cli/cli_contracts.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
AddressComputer,
Message,
ProxyNetworkProvider,
SmartContractController,
Transaction,
TransactionsFactoryConfig,
)
from multiversx_sdk.abi import Abi

from multiversx_sdk_cli import cli_shared, utils
from multiversx_sdk_cli.args_converter import convert_args_to_typed_values
from multiversx_sdk_cli.args_validation import (
validate_broadcast_args,
validate_chain_id_args,
Expand All @@ -28,9 +29,8 @@
from multiversx_sdk_cli.config_env import MxpyEnv
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
from multiversx_sdk_cli.docker import is_docker_installed, run_docker
from multiversx_sdk_cli.errors import DockerMissingError
from multiversx_sdk_cli.errors import BadUsage, DockerMissingError, QueryContractError
from multiversx_sdk_cli.ux import show_warning

logger = logging.getLogger("cli.contracts")
Expand Down Expand Up @@ -295,6 +295,7 @@ def _add_arguments_arg(sub: Any):
sub.add_argument(
"--arguments",
nargs="+",
default=[],
Copy link

Copilot AI Sep 23, 2025

Choose a reason for hiding this comment

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

Adding a default value of empty list could change existing behavior. If previous code expected None when no arguments were provided, this could cause issues. Consider checking if this change is backward compatible.

Suggested change
default=[],

Copilot uses AI. Check for mistakes.
help="arguments for the contract transaction, as [number, bech32-address, ascii string, "
"boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true addr:erd1[..]",
)
Expand All @@ -315,44 +316,61 @@ def build(args: Any):
show_warning(message)


def _initialize_controller(args: Any) -> SmartContractController:
chain_id = cli_shared.get_chain_id(args.proxy, args.chain)
config = get_config_for_network_providers()
proxy_url = args.proxy if args.proxy else ""
proxy = ProxyNetworkProvider(url=proxy_url, config=config)
abi = Abi.load(Path(args.abi)) if args.abi else None
gas_estimator = cli_shared.initialize_gas_limit_estimator(args)

return SmartContractController(
chain_id=chain_id,
network_provider=proxy,
abi=abi,
gas_limit_estimator=gas_estimator,
)


def _ensure_args_for_gas_estimation(args: Any):
if not args.proxy and not args.gas_limit:
raise BadUsage("To estimate the gas limit, you need to provide `--proxy` or set a value using `--gas-limit`")


def deploy(args: Any):
logger.debug("deploy")

validate_transaction_args(args)
validate_broadcast_args(args)
validate_chain_id_args(args)

_ensure_args_for_gas_estimation(args)

sender = cli_shared.prepare_sender(args)
guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data(
sender=sender.address.to_bech32(),
args=args,
)

chain_id = cli_shared.get_chain_id(args.proxy, args.chain)
config = TransactionsFactoryConfig(chain_id)

abi = Abi.load(Path(args.abi)) if args.abi else None
gas_estimator = cli_shared.initialize_gas_limit_estimator(args)
contract = SmartContract(config, abi, gas_estimator)

arguments, should_prepare_args = _get_contract_arguments(args)
if should_prepare_args:
arguments = convert_args_to_typed_values(arguments)

tx = contract.prepare_deploy_transaction(
owner=sender,
controller = _initialize_controller(args)
tx = controller.create_transaction_for_deploy(
sender=sender,
nonce=sender.nonce,
bytecode=Path(args.bytecode),
arguments=arguments,
should_prepare_args=should_prepare_args,
upgradeable=args.metadata_upgradeable,
readable=args.metadata_readable,
payable=args.metadata_payable,
payable_by_sc=args.metadata_payable_by_sc,
native_transfer_amount=int(args.value),
is_upgradeable=args.metadata_upgradeable,
is_readable=args.metadata_readable,
is_payable=args.metadata_payable,
is_payable_by_sc=args.metadata_payable_by_sc,
guardian=guardian_and_relayer_data.guardian.address if guardian_and_relayer_data.guardian else None,
relayer=guardian_and_relayer_data.relayer.address if guardian_and_relayer_data.relayer else None,
gas_limit=args.gas_limit,
gas_price=int(args.gas_price),
value=int(args.value),
nonce=sender.nonce,
version=int(args.version),
options=int(args.options),
guardian_and_relayer_data=guardian_and_relayer_data,
gas_price=args.gas_price,
)

address_computer = AddressComputer(NUMBER_OF_SHARDS)
Expand All @@ -361,8 +379,18 @@ def deploy(args: Any):
logger.info("Contract address: %s", contract_address.to_bech32())

cli_config = MxpyEnv.from_active_env()
utils.log_explorer_contract_address(args.chain, contract_address.to_bech32(), cli_config.explorer_url)
utils.log_explorer_contract_address(
chain=cli_shared.get_chain_id(args.proxy, args.chain),
address=contract_address.to_bech32(),
explorer_url=cli_config.explorer_url,
)

cli_shared.alter_transaction_and_sign_again_if_needed(
args=args,
tx=tx,
sender=sender,
guardian_and_relayer_data=guardian_and_relayer_data,
)
_send_or_simulate(tx, contract_address, args)


Expand All @@ -373,42 +401,45 @@ def call(args: Any):
validate_broadcast_args(args)
validate_chain_id_args(args)

_ensure_args_for_gas_estimation(args)

sender = cli_shared.prepare_sender(args)
guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data(
sender=sender.address.to_bech32(),
args=args,
)

chain_id = cli_shared.get_chain_id(args.proxy, args.chain)
config = TransactionsFactoryConfig(chain_id)

abi = Abi.load(Path(args.abi)) if args.abi else None
gas_estimator = cli_shared.initialize_gas_limit_estimator(args)
contract = SmartContract(config, abi, gas_estimator)

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

token_transfers = None
token_transfers = []
if args.token_transfers:
token_transfers = cli_shared.prepare_token_transfers(args.token_transfers)

tx = contract.prepare_execute_transaction(
caller=sender,
contract_address = Address.new_from_bech32(args.contract)
controller = _initialize_controller(args)

tx = controller.create_transaction_for_execute(
sender=sender,
nonce=sender.nonce,
contract=contract_address,
function=args.function,
arguments=arguments,
should_prepare_args=should_prepare_args,
gas_limit=args.gas_limit,
gas_price=int(args.gas_price),
value=int(args.value),
native_transfer_amount=int(args.value),
token_transfers=token_transfers,
nonce=sender.nonce,
version=int(args.version),
options=int(args.options),
guardian_and_relayer_data=guardian_and_relayer_data,
guardian=guardian_and_relayer_data.guardian.address if guardian_and_relayer_data.guardian else None,
relayer=guardian_and_relayer_data.relayer.address if guardian_and_relayer_data.relayer else None,
gas_limit=args.gas_limit,
gas_price=args.gas_price,
)

cli_shared.alter_transaction_and_sign_again_if_needed(
args=args,
tx=tx,
sender=sender,
guardian_and_relayer_data=guardian_and_relayer_data,
)
_send_or_simulate(tx, contract_address, args)


Expand All @@ -419,41 +450,44 @@ def upgrade(args: Any):
validate_broadcast_args(args)
validate_chain_id_args(args)

_ensure_args_for_gas_estimation(args)

sender = cli_shared.prepare_sender(args)
guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data(
sender=sender.address.to_bech32(),
args=args,
)

chain_id = cli_shared.get_chain_id(args.proxy, args.chain)
config = TransactionsFactoryConfig(chain_id)

abi = Abi.load(Path(args.abi)) if args.abi else None
gas_estimator = cli_shared.initialize_gas_limit_estimator(args)
contract = SmartContract(config, abi, gas_estimator)

arguments, should_prepare_args = _get_contract_arguments(args)
if should_prepare_args:
arguments = convert_args_to_typed_values(arguments)

contract_address = Address.new_from_bech32(args.contract)
controller = _initialize_controller(args)

tx = contract.prepare_upgrade_transaction(
owner=sender,
tx = controller.create_transaction_for_upgrade(
sender=sender,
nonce=sender.nonce,
contract=contract_address,
bytecode=Path(args.bytecode),
arguments=arguments,
should_prepare_args=should_prepare_args,
upgradeable=args.metadata_upgradeable,
readable=args.metadata_readable,
payable=args.metadata_payable,
payable_by_sc=args.metadata_payable_by_sc,
native_transfer_amount=int(args.value),
is_upgradeable=args.metadata_upgradeable,
is_readable=args.metadata_readable,
is_payable=args.metadata_payable,
is_payable_by_sc=args.metadata_payable_by_sc,
guardian=guardian_and_relayer_data.guardian.address if guardian_and_relayer_data.guardian else None,
relayer=guardian_and_relayer_data.relayer.address if guardian_and_relayer_data.relayer else None,
gas_limit=args.gas_limit,
gas_price=int(args.gas_price),
value=int(args.value),
nonce=sender.nonce,
version=int(args.version),
options=int(args.options),
guardian_and_relayer_data=guardian_and_relayer_data,
gas_price=args.gas_price,
)

cli_shared.alter_transaction_and_sign_again_if_needed(
args=args,
tx=tx,
sender=sender,
guardian_and_relayer_data=guardian_and_relayer_data,
)
_send_or_simulate(tx, contract_address, args)


Expand All @@ -462,26 +496,32 @@ def query(args: Any):

validate_proxy_argument(args)

# we don't need chainID to query a contract; we use the provided proxy
factory_config = TransactionsFactoryConfig("")
abi = Abi.load(Path(args.abi)) if args.abi else None
contract = SmartContract(factory_config, abi)
contract_address = Address.new_from_bech32(args.contract)
function = args.function

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

network_provider_config = get_config_for_network_providers()
proxy = ProxyNetworkProvider(url=args.proxy, config=network_provider_config)
function = args.function

result = contract.query_contract(
contract_address=contract_address,
proxy=proxy,
function=function,
arguments=arguments,
should_prepare_args=should_prepare_args,
controller = SmartContractController(
chain_id="",
network_provider=proxy,
abi=abi,
)

try:
result = controller.query(
contract=contract_address,
function=function,
arguments=arguments,
)
except Exception as e:
raise QueryContractError("Couldn't query contract: ", e)

utils.dump_out_json(result)


Expand Down
Loading
Loading