├── .ctags ├── .flake8 ├── .github ├── release.yml └── workflows │ ├── build-windows.yml │ ├── build.yml │ ├── install-macos-pipx.yml │ ├── install-ubuntu-pipx.yml │ ├── mypy.yml │ ├── publish.yml │ ├── test-localnet-tests.yml │ └── test-localnet.yml ├── .gitignore ├── .notags ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── CLI.md ├── CLI.md.sh ├── LICENSE ├── README.md ├── multiversx_sdk_cli ├── __init__.py ├── args_validation.py ├── base_transactions_controller.py ├── cli.py ├── cli_config.py ├── cli_contracts.py ├── cli_data.py ├── cli_delegation.py ├── cli_deps.py ├── cli_dns.py ├── cli_faucet.py ├── cli_ledger.py ├── cli_localnet.py ├── cli_output.py ├── cli_password.py ├── cli_shared.py ├── cli_transactions.py ├── cli_validator_wallet.py ├── cli_validators.py ├── cli_wallet.py ├── config.py ├── constants.py ├── contract_verification.py ├── contracts.py ├── cosign_transaction.py ├── delegation.py ├── dependencies │ ├── __init__.py │ ├── install.py │ ├── modules.py │ └── resolution.py ├── dns.py ├── docker.py ├── downloader.py ├── errors.py ├── guardian_relayer_data.py ├── guards.py ├── interfaces.py ├── localnet │ ├── __init__.py │ ├── config_default.py │ ├── config_general.py │ ├── config_networking.py │ ├── config_part.py │ ├── config_root.py │ ├── config_sharding.py │ ├── config_software.py │ ├── constants.py │ ├── genesis.py │ ├── genesis_json.py │ ├── genesis_smart_contracts_json.py │ ├── libraries.py │ ├── node.py │ ├── node_config_toml.py │ ├── nodes_setup_json.py │ ├── p2p_toml.py │ ├── seednode_p2pKey.pem │ ├── step_build_software.py │ ├── step_clean.py │ ├── step_config.py │ ├── step_new.py │ ├── step_prerequisites.py │ ├── step_start.py │ └── wallets.py ├── myprocess.py ├── sign_verify.py ├── simulation.py ├── tests │ ├── __init__.py │ ├── conftest.py │ ├── local_verify_server.py │ ├── shared.sh │ ├── test_accounts.py │ ├── test_cli_contracts.py │ ├── test_cli_contracts.sh │ ├── test_cli_deps.py │ ├── test_cli_dns.py │ ├── test_cli_localnet.sh │ ├── test_cli_staking_provider.py │ ├── test_cli_transactions.py │ ├── test_cli_validator_wallet.py │ ├── test_cli_validators.py │ ├── test_cli_validators_localnet.py │ ├── test_cli_wallet.py │ ├── test_contracts.py │ ├── test_playground_proxy.py │ ├── test_proxy.py │ ├── test_rust.py │ ├── test_sign.py │ ├── test_testnet.py │ ├── test_transactions.py │ └── testdata │ │ ├── adder.abi.json │ │ ├── adder.wasm │ │ ├── alice.json │ │ ├── alice.pem │ │ ├── aliceWithKindSecretKey.json │ │ ├── call_multisig_propose_batch_args.json │ │ ├── deploy_multisig_args.json │ │ ├── localnet_with_resolution_local.toml │ │ ├── localnet_with_resolution_remote.toml │ │ ├── mnemonic.txt │ │ ├── multipleValidatorsKeys.pem │ │ ├── multiple_addresses.pem │ │ ├── multisig.abi.json │ │ ├── multisig.wasm │ │ ├── testUser.json │ │ ├── testUser.pem │ │ ├── testUser2.json │ │ ├── testUser2.pem │ │ ├── transaction.json │ │ ├── upgrade_multisig_args.json │ │ ├── validator_01.pem │ │ ├── validator_02.pem │ │ ├── validators.pem │ │ ├── validators_ci.pem │ │ ├── validators_file.pem │ │ ├── walletKey.pem │ │ └── withDummyMnemonic.json ├── transactions.py ├── utils.py ├── ux.py ├── validators.py ├── version.py └── workstation.py ├── mypy.ini ├── pyproject.toml ├── pyrightconfig.json ├── pytest.ini ├── requirements-dev.txt └── requirements.txt /.ctags: -------------------------------------------------------------------------------- 1 | --exclude=*.html 2 | --exclude=*.md 3 | --exclude=.git 4 | --exclude=.gitignore 5 | --exclude=.vscode 6 | --exclude=README* 7 | --exclude=multiversx_sdk_cli/tests/testdata/* 8 | --exclude=__pycache__ 9 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | extend-ignore = E501, E722, E203 3 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release-notes 5 | categories: 6 | - title: What's Changed 7 | labels: 8 | - "*" 9 | -------------------------------------------------------------------------------- /.github/workflows/build-windows.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | pull_request: 8 | branches: [main, feat/*] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: Build and Test mxpy for ${{ matrix.os }}, python ${{ matrix.python-version }} 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [windows-2019] 20 | python-version: [3.11] 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Set up Python ${{ matrix.python-version }} 25 | uses: actions/setup-python@v4 26 | with: 27 | python-version: ${{ matrix.python-version }} 28 | - name: Install dependencies 29 | shell: bash 30 | run: | 31 | python3 -m pip install --upgrade pip 32 | pip3 install -r requirements.txt 33 | pip3 install -r requirements-dev.txt 34 | - name: Set github_api_token 35 | shell: bash 36 | run: | 37 | mkdir ~/multiversx-sdk 38 | export PYTHONPATH=. 39 | python3 -m multiversx_sdk_cli.cli config new test 40 | python3 -m multiversx_sdk_cli.cli config set github_api_token ${{ secrets.GITHUB_TOKEN }} 41 | - name: Install Rust 42 | uses: actions-rust-lang/setup-rust-toolchain@v1 43 | with: 44 | toolchain: stable 45 | target: wasm32-unknown-unknown 46 | - name: Setup test dependencies 47 | shell: bash 48 | run: | 49 | python3 -m multiversx_sdk_cli.cli deps install testwallets 50 | - name: Run unit tests 51 | shell: bash 52 | run: | 53 | export PYTHONPATH=. 54 | pytest -m "not skip_on_windows and not require_localnet" . 55 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: build 5 | 6 | on: 7 | pull_request: 8 | branches: [main, feat/*] 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: Build and Test mxpy for ${{ matrix.os }}, python ${{ matrix.python-version }} 14 | 15 | runs-on: ${{ matrix.os }} 16 | 17 | strategy: 18 | matrix: 19 | os: [ubuntu-latest, macos-latest] 20 | python-version: [3.11] 21 | max-parallel: 1 # This ensures jobs run sequentially, not concurrently 22 | 23 | steps: 24 | - uses: actions/checkout@v2 25 | - name: Set up Python ${{ matrix.python-version }} 26 | uses: actions/setup-python@v4 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | - name: Install dependencies 30 | run: | 31 | python3 -m pip install --upgrade pip 32 | pip3 install -r requirements.txt 33 | pip3 install -r requirements-dev.txt 34 | - name: Set github_api_token 35 | run: | 36 | mkdir ~/multiversx-sdk 37 | export PYTHONPATH=. 38 | python3 -m multiversx_sdk_cli.cli config new test 39 | python3 -m multiversx_sdk_cli.cli config set github_api_token ${{ secrets.GITHUB_TOKEN }} 40 | - name: Install Rust 41 | uses: actions-rust-lang/setup-rust-toolchain@v1 42 | with: 43 | toolchain: stable 44 | target: wasm32-unknown-unknown 45 | - name: Setup test dependencies 46 | run: | 47 | python3 -m multiversx_sdk_cli.cli deps install testwallets 48 | - name: Run unit tests 49 | run: | 50 | export PYTHONPATH=. 51 | pytest . 52 | -------------------------------------------------------------------------------- /.github/workflows/install-macos-pipx.yml: -------------------------------------------------------------------------------- 1 | name: Install mxpy using pipx (MacOS) 2 | 3 | on: 4 | pull_request: 5 | branches: [main, feat/*] 6 | workflow_dispatch: 7 | 8 | env: 9 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 10 | 11 | jobs: 12 | install: 13 | runs-on: macos-latest 14 | 15 | strategy: 16 | matrix: 17 | python-version: [3.11] 18 | 19 | steps: 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install pipx 25 | run: | 26 | brew install pipx 27 | pipx ensurepath 28 | - name: Install mxpy 29 | run: | 30 | pipx install git+https://github.com/multiversx/mx-sdk-py-cli@$BRANCH_NAME 31 | - name: Smoke test 32 | run: | 33 | mxpy --version 34 | -------------------------------------------------------------------------------- /.github/workflows/install-ubuntu-pipx.yml: -------------------------------------------------------------------------------- 1 | name: Install mxpy using pipx (Ubuntu) 2 | 3 | on: 4 | pull_request: 5 | branches: [main, feat/*] 6 | workflow_dispatch: 7 | 8 | env: 9 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 10 | 11 | jobs: 12 | install: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | python-version: [3.11] 18 | 19 | steps: 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install pipx 25 | run: | 26 | sudo apt update 27 | sudo apt install pipx 28 | pipx ensurepath 29 | - name: Install mxpy 30 | run: | 31 | pipx install git+https://github.com/multiversx/mx-sdk-py-cli@$BRANCH_NAME 32 | - name: Smoke test 33 | run: | 34 | mxpy --version 35 | -------------------------------------------------------------------------------- /.github/workflows/mypy.yml: -------------------------------------------------------------------------------- 1 | name: mypy reviewdog check 2 | on: [pull_request] 3 | jobs: 4 | mypy: 5 | name: runner / mypy 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v2 9 | - name: Install Python dependencies 10 | uses: py-actions/py-dependency-install@v2 11 | - name: Install dev dependencies 12 | run: pip install -r requirements-dev.txt 13 | - uses: tsuyoshicho/action-mypy@v3 14 | with: 15 | github_token: ${{ secrets.github_token }} 16 | filter_mode: nofilter 17 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Upload Python Package 10 | 11 | on: 12 | workflow_dispatch: 13 | release: 14 | types: [published] 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | deploy-mx-sdk-py-cli: 21 | 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v3 26 | - name: Set up Python 27 | uses: actions/setup-python@v3 28 | with: 29 | python-version: '3.x' 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip 33 | pip install build 34 | - name: Build package 35 | run: python -m build 36 | - name: Publish package 37 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 38 | with: 39 | user: __token__ 40 | password: ${{ secrets.PYPI_API_TOKEN }} 41 | 42 | deploy-mxpy: 43 | runs-on: ubuntu-latest 44 | needs: deploy-mx-sdk-py-cli # Ensure main package is published first 45 | 46 | steps: 47 | - uses: actions/checkout@v3 48 | 49 | - name: Set up Python 50 | uses: actions/setup-python@v3 51 | with: 52 | python-version: '3.x' 53 | 54 | - name: Install dependencies 55 | run: | 56 | python -m pip install --upgrade pip 57 | pip install build 58 | 59 | - name: Change package name for mxpy 60 | run: sed -i 's/name = "multiversx-sdk-cli"/name = "mxpy"/' pyproject.toml 61 | 62 | - name: Build and publish mxpy package 63 | run: python -m build 64 | 65 | - name: Publish mxpy package 66 | uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 67 | with: 68 | user: __token__ 69 | password: ${{ secrets.PYPI_API_TOKEN }} 70 | -------------------------------------------------------------------------------- /.github/workflows/test-localnet-tests.yml: -------------------------------------------------------------------------------- 1 | name: Test localnet-dependent tests 2 | 3 | on: 4 | pull_request: 5 | branches: [main, feat/*] 6 | workflow_dispatch: 7 | 8 | env: 9 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 10 | 11 | jobs: 12 | localnet: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | python-version: [3.11] 18 | 19 | steps: 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - uses: actions/checkout@v2 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | 31 | - name: Install dependencies 32 | run: | 33 | python3 -m pip install --upgrade pip 34 | pip3 install -r requirements.txt 35 | pip3 install -r ./requirements-dev.txt --upgrade 36 | 37 | - name: Set up MultiversX localnet 38 | run: | 39 | mkdir -p ~/multiversx-sdk 40 | export PYTHONPATH=. 41 | python3 -m multiversx_sdk_cli.cli localnet prerequisites --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 42 | python3 -m multiversx_sdk_cli.cli localnet build --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 43 | 44 | # "Go" and artifacts from "GOPATH/pkg/mod" are not needed anymore. 45 | sudo rm -rf ~/multiversx-sdk/golang 46 | 47 | python3 -m multiversx_sdk_cli.cli localnet clean --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 48 | python3 -m multiversx_sdk_cli.cli localnet config --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 49 | nohup python3 -m multiversx_sdk_cli.cli localnet start --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml > localnet.log 2>&1 & echo $! > localnet.pid 50 | sleep 120 51 | 52 | - name: Test localnet dependent tests 53 | run: | 54 | pytest -m require_localnet . 55 | python3 -m multiversx_sdk_cli.cli localnet clean --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 56 | -------------------------------------------------------------------------------- /.github/workflows/test-localnet.yml: -------------------------------------------------------------------------------- 1 | name: Test localnet 2 | 3 | on: 4 | pull_request: 5 | branches: [main, feat/*] 6 | workflow_dispatch: 7 | 8 | env: 9 | BRANCH_NAME: ${{ github.head_ref || github.ref_name }} 10 | 11 | jobs: 12 | localnet: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | python-version: [3.11] 18 | 19 | steps: 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | 25 | - uses: actions/checkout@v2 26 | - name: Set up Python ${{ matrix.python-version }} 27 | uses: actions/setup-python@v2 28 | with: 29 | python-version: ${{ matrix.python-version }} 30 | 31 | - name: Install dependencies 32 | run: | 33 | python3 -m pip install --upgrade pip 34 | pip3 install -r requirements.txt 35 | 36 | - name: Smoke test (with resolution == remote) 37 | run: | 38 | mkdir -p ~/multiversx-sdk 39 | export PYTHONPATH=. 40 | python3 -m multiversx_sdk_cli.cli config set github_api_token ${{ secrets.GITHUB_TOKEN }} 41 | python3 -m multiversx_sdk_cli.cli localnet prerequisites --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 42 | python3 -m multiversx_sdk_cli.cli localnet build --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 43 | 44 | # "Go" and artifacts from "GOPATH/pkg/mod" are not needed anymore. 45 | sudo rm -rf ~/multiversx-sdk/golang 46 | 47 | python3 -m multiversx_sdk_cli.cli localnet clean --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 48 | python3 -m multiversx_sdk_cli.cli localnet config --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml 49 | python3 -m multiversx_sdk_cli.cli localnet start --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml --stop-after-seconds=120 50 | 51 | if grep -r --include=\*.log "started committing block" ./localnet; then 52 | echo "The localnet processed blocks successfully." 53 | else 54 | echo "The localnet failed to process blocks." 55 | exit 1 56 | fi 57 | 58 | - name: Smoke test (with resolution == local) 59 | run: | 60 | mkdir -p ~/multiversx-sdk/sandbox 61 | export PYTHONPATH=. 62 | 63 | git clone https://github.com/multiversx/mx-chain-go --branch=master --single-branch ~/multiversx-sdk/sandbox/mx-chain-go 64 | git clone https://github.com/multiversx/mx-chain-proxy-go --branch=master --single-branch ~/multiversx-sdk/sandbox/mx-chain-proxy-go 65 | 66 | python3 -m multiversx_sdk_cli.cli localnet prerequisites --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml 67 | python3 -m multiversx_sdk_cli.cli localnet build --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml 68 | 69 | # "Go" and artifacts from "GOPATH/pkg/mod" are not needed anymore. 70 | sudo rm -rf ~/multiversx-sdk/golang 71 | 72 | python3 -m multiversx_sdk_cli.cli localnet clean --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml 73 | python3 -m multiversx_sdk_cli.cli localnet config --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml 74 | python3 -m multiversx_sdk_cli.cli localnet start --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml --stop-after-seconds=120 75 | 76 | if grep -r --include=\*.log "started committing block" ./localnet; then 77 | echo "The localnet processed blocks successfully." 78 | else 79 | echo "The localnet failed to process blocks." 80 | exit 1 81 | fi 82 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .eggs 2 | dist 3 | __pycache__ 4 | *.egg-info 5 | *.py[cod] 6 | *$py.class 7 | .pytest_cache/ 8 | .idea 9 | 10 | 11 | # mypy 12 | .mypy_cache/ 13 | 14 | # Sphinx documentation 15 | docs/_build/ 16 | 17 | # Tags generated by ctags 18 | tags 19 | 20 | # Environments 21 | .env 22 | .venv 23 | env/ 24 | venv/ 25 | ENV/ 26 | env.bak/ 27 | venv.bak/ 28 | 29 | # Typings 30 | typings 31 | 32 | multiversx_sdk_cli/tests/testdata-out 33 | -------------------------------------------------------------------------------- /.notags: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-py-cli/e2da34bd5cb490ad4d99af4dfcffa54e44c1d4af/.notags -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | - id: end-of-file-fixer 7 | - id: trailing-whitespace 8 | - repo: https://github.com/psf/black 9 | rev: 24.10.0 10 | hooks: 11 | - id: black 12 | args: [--line-length=120] 13 | - repo: https://github.com/pycqa/isort 14 | rev: 5.13.2 15 | hooks: 16 | - id: isort 17 | name: isort (python) 18 | args: ["--profile", "black", "--filter-files"] 19 | - repo: https://github.com/PyCQA/flake8 20 | rev: 7.1.1 21 | hooks: 22 | - id: flake8 23 | args: 24 | - "--config=.flake8" 25 | - repo: https://github.com/PyCQA/autoflake 26 | rev: v2.3.1 27 | hooks: 28 | - id: autoflake 29 | args: 30 | - --in-place 31 | - --remove-all-unused-imports 32 | - repo: https://github.com/RobertCraigie/pyright-python 33 | rev: v1.1.392 34 | hooks: 35 | - id: pyright 36 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": ["multiversx_sdk_cli"], 3 | "python.testing.pytestEnabled": true, 4 | "editor.formatOnSave": true, 5 | "[python]": { 6 | "editor.defaultFormatter": "ms-python.black-formatter" 7 | }, 8 | "editor.codeActionsOnSave": { 9 | "source.organizeImports": "explicit" 10 | }, 11 | "files.insertFinalNewline": true, 12 | "python.languageServer": "Pylance", 13 | "files.autoSave": "onWindowChange", 14 | "editor.fontSize": 13, 15 | "files.trimTrailingWhitespace": true, 16 | "github.gitProtocol": "ssh", 17 | "python.analysis.autoFormatStrings": true, 18 | "python.analysis.completeFunctionParens": true, 19 | "black-formatter.args": ["--line-length=120"] 20 | } 21 | -------------------------------------------------------------------------------- /CLI.md.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export PYTHONPATH=. 4 | 5 | CLI="python3 -m multiversx_sdk_cli.cli" 6 | CLI_ALIAS="mxpy" 7 | 8 | code() { 9 | printf "\n\`\`\`\n" >> CLI.md 10 | } 11 | 12 | group() { 13 | printf "## Group **$1**\n\n" >> CLI.md 14 | 15 | code 16 | printf "$ ${CLI_ALIAS} $2 --help\n" >> CLI.md 17 | ${CLI} ${2} --help >> CLI.md 18 | code 19 | } 20 | 21 | command() { 22 | printf "### $1\n\n" >> CLI.md 23 | 24 | code 25 | printf "$ ${CLI_ALIAS} $2 --help\n" >> CLI.md 26 | ${CLI} ${2} --help >> CLI.md 27 | code 28 | } 29 | 30 | generate() { 31 | echo -n > CLI.md 32 | printf "# Command Line Interface\n\n" >> CLI.md 33 | 34 | printf "## Overview\n\n" >> CLI.md 35 | printf "**${CLI_ALIAS}** exposes a number of CLI **commands**, organized within **groups**.\n\n" >> CLI.md 36 | 37 | code 38 | printf "$ ${CLI_ALIAS} --help\n" >> CLI.md 39 | ${CLI} --help >> CLI.md 40 | code 41 | 42 | group "Contract" "contract" 43 | command "Contract.Deploy" "contract deploy" 44 | command "Contract.Call" "contract call" 45 | command "Contract.Upgrade" "contract upgrade" 46 | command "Contract.Query" "contract query" 47 | command "Contract.Verify" "contract verify" 48 | command "Contract.ReproducibleBuild" "contract reproducible-build" 49 | 50 | group "Transactions" "tx" 51 | command "Transactions.New" "tx new" 52 | command "Transactions.Send" "tx send" 53 | command "Transactions.Sign" "tx sign" 54 | command "Transactions.Relay" "tx relay" 55 | 56 | group "Validator" "validator" 57 | command "Validator.Stake" "validator stake" 58 | command "Validator.Unstake" "validator unstake" 59 | command "Validator.Unjail" "validator unjail" 60 | command "Validator.Unbond" "validator unbond" 61 | command "Validator.ChangeRewardAddress" "validator change-reward-address" 62 | command "Validator.Claim" "validator claim" 63 | command "Validator.UnstakeNodes" "validator unstake-nodes" 64 | command "Validator.UnstakeTokens" "validator unstake-tokens" 65 | command "Validator.UnbondNodes" "validator unbond-nodes" 66 | command "Validator.UnbondTokens" "validator unbond-tokens" 67 | command "Validator.CleanRegisteredData" "validator clean-registered-data" 68 | command "Validator.RestakeUnstakedNodes" "validator restake-unstaked-nodes" 69 | 70 | group "StakingProvider" "staking-provider" 71 | command "StakingProvider.CreateNewDelegationContract" "staking-provider create-new-delegation-contract" 72 | command "StakingProvider.GetContractAddress" "staking-provider get-contract-address" 73 | command "StakingProvider.AddNodes" "staking-provider add-nodes" 74 | command "StakingProvider.RemoveNodes" "staking-provider remove-nodes" 75 | command "StakingProvider.StakeNodes" "staking-provider stake-nodes" 76 | command "StakingProvider.UnbondNodes" "staking-provider unbond-nodes" 77 | command "StakingProvider.UnstakeNodes" "staking-provider unstake-nodes" 78 | command "StakingProvider.UnjailNodes" "staking-provider unjail-nodes" 79 | command "StakingProvider.Delegate" "staking-provider delegate" 80 | command "StakingProvider.ClaimRewards" "staking-provider claim-rewards" 81 | command "StakingProvider.RedelegateRewards" "staking-provider redelegate-rewards" 82 | command "StakingProvider.Undelegate" "staking-provider undelegate" 83 | command "StakingProvider.Withdraw" "staking-provider withdraw" 84 | command "StakingProvider.ChangeServiceFee" "staking-provider change-service-fee" 85 | command "StakingProvider.ModifyDelegationCap" "staking-provider modify-delegation-cap" 86 | command "StakingProvider.AutomaticActivation" "staking-provider automatic-activation" 87 | command "StakingProvider.RedelegateCap" "staking-provider redelegate-cap" 88 | command "StakingProvider.SetMetadata" "staking-provider set-metadata" 89 | command "StakingProvider.MakeDelegationContractFromValidator" "staking-provider make-delegation-contract-from-validator" 90 | 91 | group "Wallet" "wallet" 92 | command "Wallet.New" "wallet new" 93 | command "Wallet.Convert" "wallet convert" 94 | command "Wallet.Bech32" "wallet bech32" 95 | command "Wallet.SignMessage" "wallet sign-message" 96 | command "Wallet.VerifyMessage" "wallet verify-message" 97 | 98 | group "ValidatorWallet" "validator-wallet" 99 | command "Wallet.New" "validator-wallet new" 100 | command "Wallet.Convert" "validator-wallet convert" 101 | command "Wallet.SignMessage" "validator-wallet sign-message" 102 | command "Wallet.VerifyMessage" "validator-wallet verify-message-signature" 103 | 104 | group "Localnet" "localnet" 105 | command "Localnet.Setup" "localnet setup" 106 | command "Localnet.New" "localnet new" 107 | command "Localnet.Prerequisites" "localnet prerequisites" 108 | command "Localnet.Build" "localnet build" 109 | command "Localnet.Config" "localnet config" 110 | command "Localnet.Start" "localnet start" 111 | command "Localnet.Clean" "localnet clean" 112 | 113 | group "Dependencies" "deps" 114 | command "Dependencies.Install" "deps install" 115 | command "Dependencies.Check" "deps check" 116 | 117 | group "Configuration" "config" 118 | command "Configuration.Dump" "config dump" 119 | command "Configuration.Get" "config get" 120 | command "Configuration.Set" "config set" 121 | command "Configuration.New" "config new" 122 | command "Configuration.Switch" "config switch" 123 | command "Configuration.List" "config list" 124 | command "Configuration.Reset" "config reset" 125 | 126 | group "Data" "data" 127 | command "Data.Dump" "data parse" 128 | command "Data.Store" "data store" 129 | command "Data.Load" "data load" 130 | 131 | group "Faucet" "faucet" 132 | command "Faucet.Request" "faucet request" 133 | } 134 | 135 | generate 136 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ## License 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) MultiversX 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Description 2 | Python Command Line Tools for interacting with MultiversX. 3 | 4 | ## Documentation 5 | [docs.multiversx.com](https://docs.multiversx.com/sdk-and-tools/sdk-py/) 6 | 7 | ## CLI 8 | [CLI](CLI.md) 9 | 10 | ## Distribution 11 | [pipx](https://docs.multiversx.com/sdk-and-tools/sdk-py/installing-mxpy/) [(PyPi)](https://pypi.org/project/multiversx-sdk-cli/#history) 12 | 13 | ## Development setup 14 | 15 | Clone this repository and cd into it: 16 | 17 | ``` 18 | git clone https://github.com/multiversx/mx-sdk-py-cli.git 19 | cd mx-sdk-py-cli 20 | ``` 21 | 22 | ### Virtual environment 23 | 24 | Create a virtual environment and install the dependencies: 25 | 26 | ``` 27 | python3 -m venv ./venv 28 | source ./venv/bin/activate 29 | pip install -r ./requirements.txt --upgrade 30 | ``` 31 | 32 | Install development dependencies, as well: 33 | 34 | ``` 35 | pip install -r ./requirements-dev.txt --upgrade 36 | ``` 37 | 38 | Allow `pre-commit` to automatically run on `git commit`: 39 | ``` 40 | pre-commit install 41 | ``` 42 | 43 | Above, `requirements.txt` should mirror the **dependencies** section of `pyproject.toml`. 44 | 45 | If using VSCode, restart it or follow these steps: 46 | - `Ctrl + Shift + P` 47 | - _Select Interpreter_ 48 | - Choose `./venv/bin/python`. 49 | 50 | ### Using your local `mxpy` 51 | 52 | If you want to test the modifications you locally made to `mxpy`, set `PYTHONPATH` with the path to your local repository path. 53 | 54 | For example, if you cloned the repository at `~/mx-sdk-py-cli`, run: 55 | 56 | ``` 57 | export PYTHONPATH="~/mx-sdk-py-cli" 58 | ``` 59 | 60 | Then `mxpy` will use the code in your local repository. 61 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-py-cli/e2da34bd5cb490ad4d99af4dfcffa54e44c1d4af/multiversx_sdk_cli/__init__.py -------------------------------------------------------------------------------- /multiversx_sdk_cli/args_validation.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from multiversx_sdk_cli.errors import InvalidArgumentsError 4 | 5 | 6 | def validate_transaction_args(args: Any): 7 | validate_nonce_args(args) 8 | validate_receiver_args(args) 9 | validate_gas_limit_args(args) 10 | 11 | 12 | def validate_nonce_args(args: Any): 13 | """If nonce is not provided, ensure that proxy is provided.""" 14 | if hasattr(args, "nonce") and args.nonce is None: 15 | 16 | if hasattr(args, "proxy") and not args.proxy: 17 | raise InvalidArgumentsError("--proxy must be provided if --nonce is not provided") 18 | 19 | 20 | def validate_receiver_args(args: Any): 21 | """Ensure that receiver is provided.""" 22 | if hasattr(args, "receiver") and not args.receiver: 23 | raise InvalidArgumentsError("--receiver must be provided") 24 | 25 | 26 | def validate_gas_limit_args(args: Any): 27 | """Ensure that gas_limit is provided.""" 28 | if hasattr(args, "gas_limit") and not args.gas_limit: 29 | raise InvalidArgumentsError("--gas-limit must be provided") 30 | 31 | 32 | def ensure_wallet_args_are_provided(args: Any): 33 | signing_methods = [args.pem, args.keyfile, args.ledger] 34 | 35 | if all(signing_methods): 36 | raise InvalidArgumentsError("Only one of --pem, --keyfile, or --ledger must be provided") 37 | 38 | if not any(signing_methods): 39 | raise InvalidArgumentsError("One of --pem, --keyfile, or --ledger must be provided") 40 | 41 | 42 | def ensure_relayer_wallet_args_are_provided(args: Any): 43 | signing_methods = [args.relayer_pem, args.relayer_keyfile, args.relayer_ledger] 44 | 45 | if all(signing_methods): 46 | raise InvalidArgumentsError( 47 | "Only one of --relayer-pem, --relayer-keyfile, or --relayer-ledger must be provided" 48 | ) 49 | 50 | if not any(signing_methods): 51 | raise InvalidArgumentsError("One of --relayer-pem, --relayer-keyfile, or --relayer-ledger must be provided") 52 | 53 | 54 | def validate_broadcast_args(args: Any): 55 | if args.send and args.simulate: 56 | raise InvalidArgumentsError("Cannot both 'simulate' and 'send' a transaction") 57 | 58 | if args.send or args.simulate: 59 | validate_proxy_argument(args) 60 | 61 | 62 | def validate_chain_id_args(args: Any): 63 | if not args.chain and not args.proxy: 64 | raise InvalidArgumentsError("Either --chain or --proxy must be provided") 65 | 66 | 67 | def validate_proxy_argument(args: Any): 68 | if not args.proxy: 69 | raise InvalidArgumentsError("--proxy must be provided") 70 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/base_transactions_controller.py: -------------------------------------------------------------------------------- 1 | from typing import Optional, Union 2 | 3 | from multiversx_sdk import LedgerAccount, Transaction, TransactionComputer 4 | 5 | from multiversx_sdk_cli.constants import ( 6 | EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS, 7 | EXTRA_GAS_LIMIT_FOR_RELAYED_TRANSACTIONS, 8 | ) 9 | from multiversx_sdk_cli.cosign_transaction import cosign_transaction 10 | from multiversx_sdk_cli.interfaces import IAccount 11 | 12 | 13 | class BaseTransactionsController: 14 | def __init__(self) -> None: 15 | pass 16 | 17 | def sign_transaction( 18 | self, 19 | transaction: Transaction, 20 | sender: Optional[IAccount] = None, 21 | guardian: Optional[IAccount] = None, 22 | relayer: Optional[IAccount] = None, 23 | guardian_service_url: Optional[str] = None, 24 | guardian_2fa_code: Optional[str] = None, 25 | ): 26 | """Signs the transaction using the sender's account and, if required, additionally signs with the guardian's and relayer's accounts. Ensures the appropriate transaction options are set as needed.""" 27 | self._set_options_for_guarded_transaction_if_needed(transaction) 28 | self._set_options_for_hash_signing_if_needed(transaction, sender, guardian, relayer) 29 | 30 | if sender: 31 | transaction.signature = sender.sign_transaction(transaction) 32 | 33 | self._sign_guarded_transaction_if_guardian( 34 | transaction, 35 | guardian, 36 | guardian_service_url, 37 | guardian_2fa_code, 38 | ) 39 | self._sign_relayed_transaction_if_relayer(transaction, relayer) 40 | 41 | def add_extra_gas_limit_if_required(self, transaction: Transaction): 42 | """In case of guarded or relayed transactions, extra gas limit is added.""" 43 | if transaction.guardian: 44 | transaction.gas_limit += EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS 45 | 46 | if transaction.relayer: 47 | transaction.gas_limit += EXTRA_GAS_LIMIT_FOR_RELAYED_TRANSACTIONS 48 | 49 | def _set_options_for_guarded_transaction_if_needed(self, transaction: Transaction): 50 | if transaction.guardian: 51 | transaction_computer = TransactionComputer() 52 | transaction_computer.apply_guardian(transaction, transaction.guardian) 53 | 54 | def _set_options_for_hash_signing_if_needed( 55 | self, 56 | transaction: Transaction, 57 | sender: Union[IAccount, None], 58 | guardian: Union[IAccount, None], 59 | relayer: Union[IAccount, None], 60 | ): 61 | if ( 62 | isinstance(sender, LedgerAccount) 63 | or isinstance(guardian, LedgerAccount) 64 | or isinstance(relayer, LedgerAccount) 65 | ): 66 | transaction_computer = TransactionComputer() 67 | transaction_computer.apply_options_for_hash_signing(transaction) 68 | 69 | def _sign_guarded_transaction_if_guardian( 70 | self, 71 | transaction: Transaction, 72 | guardian: Union[IAccount, None], 73 | guardian_service_url: Union[str, None], 74 | guardian_2fa_code: Union[str, None], 75 | ) -> Transaction: 76 | # If the guardian account is provided, we sign locally. Otherwise, we reach for the trusted cosign service. 77 | if guardian: 78 | transaction.guardian_signature = guardian.sign_transaction(transaction) 79 | elif transaction.guardian and guardian_service_url and guardian_2fa_code: 80 | cosign_transaction(transaction, guardian_service_url, guardian_2fa_code) 81 | 82 | return transaction 83 | 84 | def _sign_relayed_transaction_if_relayer(self, transaction: Transaction, relayer: Union[IAccount, None]): 85 | if relayer and transaction.relayer: 86 | transaction.relayer_signature = relayer.sign_transaction(transaction) 87 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli.py: -------------------------------------------------------------------------------- 1 | # PYTHON_ARGCOMPLETE_OK 2 | import argparse 3 | import logging 4 | import sys 5 | from argparse import ArgumentParser 6 | from typing import Any 7 | 8 | import argcomplete 9 | from multiversx_sdk import LibraryConfig 10 | from rich.logging import RichHandler 11 | 12 | import multiversx_sdk_cli.cli_config 13 | import multiversx_sdk_cli.cli_contracts 14 | import multiversx_sdk_cli.cli_data 15 | import multiversx_sdk_cli.cli_delegation 16 | import multiversx_sdk_cli.cli_deps 17 | import multiversx_sdk_cli.cli_dns 18 | import multiversx_sdk_cli.cli_faucet 19 | import multiversx_sdk_cli.cli_ledger 20 | import multiversx_sdk_cli.cli_localnet 21 | import multiversx_sdk_cli.cli_transactions 22 | import multiversx_sdk_cli.cli_validator_wallet 23 | import multiversx_sdk_cli.cli_validators 24 | import multiversx_sdk_cli.cli_wallet 25 | import multiversx_sdk_cli.version 26 | from multiversx_sdk_cli import config, errors, utils, ux 27 | 28 | logger = logging.getLogger("cli") 29 | 30 | 31 | def main(cli_args: list[str] = sys.argv[1:]): 32 | try: 33 | _do_main(cli_args) 34 | except errors.KnownError as err: 35 | logger.critical(err.get_pretty()) 36 | ux.show_critical_error(err.get_pretty()) 37 | return 1 38 | except KeyboardInterrupt: 39 | print("process killed by user.") 40 | return 1 41 | return 0 42 | 43 | 44 | def _do_main(cli_args: list[str]): 45 | utils.ensure_folder(config.SDK_PATH) 46 | argv_with_config_args = config.add_config_args(cli_args) 47 | parser = setup_parser(argv_with_config_args) 48 | argcomplete.autocomplete(parser) 49 | args = parser.parse_args(argv_with_config_args) 50 | 51 | if args.verbose: 52 | logging.basicConfig( 53 | level="DEBUG", 54 | force=True, 55 | format="%(name)s: %(message)s", 56 | handlers=[RichHandler(show_time=False, rich_tracebacks=True)], 57 | ) 58 | else: 59 | logging.basicConfig( 60 | level="INFO", 61 | format="%(name)s: %(message)s", 62 | handlers=[RichHandler(show_time=False, rich_tracebacks=True)], 63 | ) 64 | 65 | verify_deprecated_entries_in_config_file() 66 | default_hrp = config.get_address_hrp() 67 | LibraryConfig.default_address_hrp = default_hrp 68 | 69 | if hasattr(args, "recall_nonce") and args.recall_nonce: 70 | logger.warning("The --recall-nonce flag is DEPRECATED. The nonce is fetched from the network by default.") 71 | 72 | if not hasattr(args, "func"): 73 | parser.print_help() 74 | else: 75 | args.func(args) 76 | 77 | 78 | def setup_parser(args: list[str]): 79 | parser = ArgumentParser( 80 | prog="mxpy", 81 | usage="mxpy [-h] [-v] [--verbose] COMMAND-GROUP [-h] COMMAND ...", 82 | description=""" 83 | ----------- 84 | DESCRIPTION 85 | ----------- 86 | mxpy is part of the multiversx-sdk and consists of Command Line Tools and Python SDK 87 | for interacting with the Blockchain (in general) and with Smart Contracts (in particular). 88 | 89 | mxpy targets a broad audience of users and developers. 90 | 91 | See: 92 | - https://docs.multiversx.com/sdk-and-tools/sdk-py 93 | - https://docs.multiversx.com/sdk-and-tools/sdk-py/mxpy-cli 94 | """, 95 | formatter_class=argparse.RawDescriptionHelpFormatter, 96 | ) 97 | 98 | parser._positionals.title = "COMMAND GROUPS" 99 | parser._optionals.title = "TOP-LEVEL OPTIONS" 100 | version = multiversx_sdk_cli.version.get_version() 101 | parser.add_argument( 102 | "-v", 103 | "--version", 104 | action="version", 105 | version=f"MultiversX Python CLI (mxpy) {version}", 106 | ) 107 | parser.add_argument("--verbose", action="store_true", default=False) 108 | 109 | subparsers = parser.add_subparsers() 110 | commands: list[Any] = [] 111 | 112 | commands.append(multiversx_sdk_cli.cli_contracts.setup_parser(args, subparsers)) 113 | commands.append(multiversx_sdk_cli.cli_transactions.setup_parser(args, subparsers)) 114 | commands.append(multiversx_sdk_cli.cli_validators.setup_parser(args, subparsers)) 115 | commands.append(multiversx_sdk_cli.cli_ledger.setup_parser(subparsers)) 116 | commands.append(multiversx_sdk_cli.cli_wallet.setup_parser(args, subparsers)) 117 | commands.append(multiversx_sdk_cli.cli_validator_wallet.setup_parser(args, subparsers)) 118 | commands.append(multiversx_sdk_cli.cli_deps.setup_parser(subparsers)) 119 | commands.append(multiversx_sdk_cli.cli_config.setup_parser(subparsers)) 120 | commands.append(multiversx_sdk_cli.cli_localnet.setup_parser(args, subparsers)) 121 | commands.append(multiversx_sdk_cli.cli_data.setup_parser(subparsers)) 122 | commands.append(multiversx_sdk_cli.cli_delegation.setup_parser(args, subparsers)) 123 | commands.append(multiversx_sdk_cli.cli_dns.setup_parser(args, subparsers)) 124 | commands.append(multiversx_sdk_cli.cli_faucet.setup_parser(args, subparsers)) 125 | 126 | parser.epilog = """ 127 | ---------------------- 128 | COMMAND GROUPS summary 129 | ---------------------- 130 | """ 131 | for choice, sub in subparsers.choices.items(): 132 | parser.epilog += f"{choice.ljust(30)} {sub.description}\n" 133 | 134 | return parser 135 | 136 | 137 | def verify_deprecated_entries_in_config_file(): 138 | deprecated_keys = config.get_deprecated_entries_in_config_file() 139 | if len(deprecated_keys) == 0: 140 | return 141 | 142 | config_path = config.resolve_config_path() 143 | message = f"The following config entries are deprecated. Please access `{str(config_path)}` and remove them. \n" 144 | for entry in deprecated_keys: 145 | message += f"-> {entry} \n" 146 | 147 | ux.show_warning(message.rstrip("\n")) 148 | 149 | 150 | if __name__ == "__main__": 151 | ret = main(sys.argv[1:]) 152 | sys.exit(ret) 153 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_config.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from typing import Any 5 | 6 | from multiversx_sdk_cli import cli_shared, config, utils 7 | from multiversx_sdk_cli.ux import confirm_continuation 8 | 9 | logger = logging.getLogger("cli.config") 10 | 11 | 12 | def setup_parser(subparsers: Any) -> Any: 13 | parser = cli_shared.add_group_subparser(subparsers, "config", "Configure multiversx-sdk (default values etc.)") 14 | subparsers = parser.add_subparsers() 15 | 16 | sub = cli_shared.add_command_subparser(subparsers, "config", "dump", "Dumps configuration.") 17 | sub.add_argument( 18 | "--defaults", 19 | required=False, 20 | help="dump defaults instead of local config", 21 | action="store_true", 22 | ) 23 | sub.set_defaults(func=dump) 24 | 25 | sub = cli_shared.add_command_subparser(subparsers, "config", "get", "Gets a configuration value.") 26 | _add_name_arg(sub) 27 | sub.set_defaults(func=get_value) 28 | 29 | sub = cli_shared.add_command_subparser(subparsers, "config", "set", "Sets a configuration value.") 30 | _add_name_arg(sub) 31 | sub.add_argument("value", help="the new value") 32 | sub.set_defaults(func=set_value) 33 | 34 | sub = cli_shared.add_command_subparser(subparsers, "config", "delete", "Deletes a configuration value.") 35 | _add_name_arg(sub) 36 | sub.set_defaults(func=delete_value) 37 | 38 | sub = cli_shared.add_command_subparser(subparsers, "config", "new", "Creates a new configuration.") 39 | _add_name_arg(sub) 40 | sub.add_argument( 41 | "--template", 42 | required=False, 43 | help="template from which to create the new config", 44 | ) 45 | sub.set_defaults(func=new_config) 46 | 47 | sub = cli_shared.add_command_subparser(subparsers, "config", "switch", "Switch to a different config") 48 | _add_name_arg(sub) 49 | sub.set_defaults(func=switch_config) 50 | 51 | sub = cli_shared.add_command_subparser(subparsers, "config", "list", "List available configs") 52 | sub.set_defaults(func=list_configs) 53 | 54 | sub = cli_shared.add_command_subparser( 55 | subparsers, 56 | "config", 57 | "reset", 58 | "Deletes the config file. Default config will be used.", 59 | ) 60 | sub.set_defaults(func=delete_config) 61 | 62 | parser.epilog = cli_shared.build_group_epilog(subparsers) 63 | return subparsers 64 | 65 | 66 | def _add_name_arg(sub: Any): 67 | sub.add_argument("name", help="the name of the configuration entry") 68 | 69 | 70 | def dump(args: Any): 71 | if args.defaults: 72 | _dump_defaults() 73 | else: 74 | _dump_active() 75 | 76 | 77 | def _dump_defaults(): 78 | utils.dump_out_json(config.get_defaults(), sys.stdout) 79 | 80 | 81 | def _dump_active(): 82 | utils.dump_out_json(config.get_active(), sys.stdout) 83 | 84 | 85 | def get_value(args: Any): 86 | value = config.get_value(args.name) 87 | print(value) 88 | 89 | 90 | def set_value(args: Any): 91 | config.set_value(args.name, args.value) 92 | 93 | 94 | def delete_value(args: Any): 95 | config.delete_value(args.name) 96 | 97 | 98 | def new_config(args: Any): 99 | config.create_new_config(name=args.name, template=args.template) 100 | _dump_active() 101 | 102 | 103 | def switch_config(args: Any): 104 | config.set_active(args.name) 105 | _dump_active() 106 | 107 | 108 | def list_configs(args: Any): 109 | data = config.read_file() 110 | configurations = data.get("configurations", {}) 111 | for config_name in configurations.keys(): 112 | if config_name == data.get("active", "default"): 113 | config_name += "*" 114 | print(config_name) 115 | 116 | 117 | def delete_config(args: Any): 118 | config_file = config.resolve_config_path() 119 | if not config_file.is_file(): 120 | logger.info("Config file not found. Aborting...") 121 | return 122 | 123 | confirm_continuation(f"The file `{str(config_file)}` will be deleted. Do you want to continue? (y/n)") 124 | os.remove(config_file) 125 | logger.info("Successfully deleted the config file") 126 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_data.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | from pathlib import Path 4 | from typing import Any 5 | 6 | from multiversx_sdk_cli import cli_shared, errors, utils, workstation 7 | 8 | logger = logging.getLogger("cli.data") 9 | 10 | DATA_FILENAME = "mxpy.data-storage.json" 11 | 12 | 13 | def setup_parser(subparsers: Any) -> Any: 14 | parser = cli_shared.add_group_subparser(subparsers, "data", "Data manipulation omnitool") 15 | subparsers = parser.add_subparsers() 16 | 17 | sub = cli_shared.add_command_subparser(subparsers, "data", "parse", "Parses values from a given file") 18 | sub.add_argument("--file", required=True, help="path of the file to parse") 19 | sub.add_argument( 20 | "--expression", 21 | required=True, 22 | help="the Python-Dictionary expression to evaluate in order to extract the data", 23 | ) 24 | sub.set_defaults(func=parse) 25 | 26 | sub = cli_shared.add_command_subparser(subparsers, "data", "store", "Stores a key-value pair within a partition") 27 | sub.add_argument("--key", required=True, help="the key") 28 | sub.add_argument("--value", required=True, help="the value to save") 29 | sub.add_argument("--partition", default="*", help="the storage partition (default: %(default)s)") 30 | sub.add_argument( 31 | "--use-global", 32 | action="store_true", 33 | default=False, 34 | help="use the global storage (default: %(default)s)", 35 | ) 36 | sub.set_defaults(func=store) 37 | 38 | sub = cli_shared.add_command_subparser( 39 | subparsers, "data", "load", "Loads a key-value pair from a storage partition" 40 | ) 41 | sub.add_argument("--key", required=True, help="the key") 42 | sub.add_argument("--partition", default="*", help="the storage partition (default: %(default)s)") 43 | sub.add_argument( 44 | "--use-global", 45 | action="store_true", 46 | default=False, 47 | help="use the global storage (default: %(default)s)", 48 | ) 49 | sub.set_defaults(func=load) 50 | 51 | parser.epilog = cli_shared.build_group_epilog(subparsers) 52 | return subparsers 53 | 54 | 55 | def parse(args: Any): 56 | file = Path(args.file).expanduser() 57 | expression: str = args.expression 58 | suffix = file.suffix 59 | data = None 60 | 61 | if suffix == ".json": 62 | data = utils.read_json_file(str(file)) 63 | else: 64 | raise errors.BadUsage(f"File isn't parsable: {file}") 65 | 66 | try: 67 | result = eval(expression, {"data": data}) 68 | except KeyError: 69 | result = "" 70 | 71 | print(result) 72 | 73 | 74 | def store(args: Any): 75 | logger.warning("Never use this command to store sensitive information! Data is unencrypted.") 76 | 77 | key = args.key 78 | value = args.value 79 | partition = args.partition 80 | use_global = args.use_global 81 | 82 | data = _read_file(use_global) 83 | if partition not in data: 84 | data[partition] = dict() 85 | 86 | data_in_partition = data[partition] 87 | data_in_partition[key] = value 88 | _write_file(use_global, data) 89 | 90 | logger.info(f"Data has been stored at key = '{key}', in partition = '{partition}'.") 91 | 92 | 93 | def load(args: Any): 94 | key = args.key 95 | partition = args.partition 96 | use_global = args.use_global 97 | 98 | data = _read_file(use_global) 99 | data_in_partition = data.get(partition, dict()) 100 | value = data_in_partition.get(key, "") 101 | print(value) 102 | 103 | 104 | def _read_file(use_global: bool) -> dict[str, Any]: 105 | filename = _get_filename(use_global) 106 | 107 | if not os.path.isfile(filename): 108 | return dict() 109 | data: dict[str, Any] = utils.read_json_file(filename) 110 | return data 111 | 112 | 113 | def _write_file(use_global: bool, data: dict[str, Any]): 114 | filename = _get_filename(use_global) 115 | 116 | utils.write_json_file(str(filename), data) 117 | 118 | 119 | def _get_filename(use_global: bool): 120 | if use_global: 121 | return workstation.get_tools_folder() / DATA_FILENAME 122 | return Path(os.getcwd()) / DATA_FILENAME 123 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_deps.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | 4 | from multiversx_sdk_cli import cli_shared, config, dependencies, errors 5 | from multiversx_sdk_cli.dependencies.install import get_deps_dict 6 | from multiversx_sdk_cli.dependencies.modules import DependencyModule 7 | 8 | logger = logging.getLogger("cli.deps") 9 | 10 | 11 | def setup_parser(subparsers: Any) -> Any: 12 | parser = cli_shared.add_group_subparser(subparsers, "deps", "Manage dependencies or multiversx-sdk modules") 13 | subparsers = parser.add_subparsers() 14 | 15 | choices = ["all"] + list(get_deps_dict().keys()) 16 | 17 | sub = cli_shared.add_command_subparser( 18 | subparsers, "deps", "install", "Install dependencies or multiversx-sdk modules." 19 | ) 20 | sub.add_argument("name", choices=choices, help="the dependency to install") 21 | sub.add_argument( 22 | "--overwrite", 23 | action="store_true", 24 | default=False, 25 | help="whether to overwrite an existing installation", 26 | ) 27 | sub.set_defaults(func=install) 28 | 29 | sub = cli_shared.add_command_subparser(subparsers, "deps", "check", "Check whether a dependency is installed.") 30 | sub.add_argument("name", choices=choices, help="the dependency to check") 31 | sub.set_defaults(func=check) 32 | 33 | parser.epilog = cli_shared.build_group_epilog(subparsers) 34 | return subparsers 35 | 36 | 37 | def install(args: Any): 38 | name: str = args.name 39 | overwrite: bool = args.overwrite 40 | dependencies.install_module(name, overwrite) 41 | 42 | 43 | def check(args: Any): 44 | name: str = args.name 45 | 46 | if name == "all": 47 | all_dependencies = dependencies.get_all_deps() 48 | missing_dependencies: list[tuple[str, str]] = [] 49 | 50 | for dependency in all_dependencies: 51 | tag_to_check: str = config.get_dependency_tag(dependency.key) 52 | is_installed = check_module_is_installed(dependency, tag_to_check) 53 | 54 | if not is_installed: 55 | missing_dependencies.append((dependency.key, tag_to_check)) 56 | 57 | if len(missing_dependencies): 58 | raise errors.DependenciesMissing(missing_dependencies) 59 | return 60 | 61 | module = dependencies.get_module_by_key(name) 62 | tag_to_check = config.get_dependency_tag(module.key) 63 | 64 | is_installed = check_module_is_installed(module, tag_to_check) 65 | if is_installed: 66 | logger.info(f"[{module.key} {tag_to_check}] is installed.") 67 | return 68 | elif not is_installed: 69 | raise errors.DependencyMissing(module.key, tag_to_check) 70 | 71 | 72 | def check_module_is_installed(module: DependencyModule, tag_to_check: str) -> bool: 73 | resolution: str = config.get_dependency_resolution(module.key) 74 | resolution = resolution if resolution else "HOST" 75 | 76 | logger.info(f"Checking dependency: module = {module.key}, tag = {tag_to_check}, resolution = {resolution}") 77 | 78 | installed = module.is_installed(tag_to_check) 79 | return installed 80 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_faucet.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import webbrowser 3 | from enum import Enum 4 | from typing import Any 5 | 6 | from multiversx_sdk import Message, NativeAuthClient, NativeAuthClientConfig 7 | 8 | from multiversx_sdk_cli import cli_shared 9 | from multiversx_sdk_cli.errors import ArgumentsNotProvidedError, BadUserInput 10 | 11 | logger = logging.getLogger("cli.faucet") 12 | 13 | 14 | class WebWalletUrls(Enum): 15 | DEVNET = "https://devnet-wallet.multiversx.com" 16 | TESTNET = "https://testnet-wallet.multiversx.com" 17 | 18 | 19 | class ApiUrls(Enum): 20 | DEVNET = "https://devnet-api.multiversx.com" 21 | TESTNET = "https://testnet-api.multiversx.com" 22 | 23 | 24 | def setup_parser(args: list[str], subparsers: Any) -> Any: 25 | parser = cli_shared.add_group_subparser(subparsers, "faucet", "Get xEGLD on Devnet or Testnet") 26 | subparsers = parser.add_subparsers() 27 | 28 | sub = cli_shared.add_command_subparser(subparsers, "faucet", "request", "Request xEGLD.") 29 | cli_shared.add_wallet_args(args, sub) 30 | sub.add_argument("--chain", choices=["D", "T"], help="the chain identifier") 31 | sub.add_argument("--api", type=str, help="custom api url for the native auth client") 32 | sub.add_argument("--wallet-url", type=str, help="custom wallet url to call the faucet from") 33 | sub.set_defaults(func=faucet) 34 | 35 | parser.epilog = cli_shared.build_group_epilog(subparsers) 36 | return subparsers 37 | 38 | 39 | def faucet(args: Any): 40 | account = cli_shared.prepare_account(args) 41 | wallet, api = get_wallet_and_api_urls(args) 42 | 43 | config = NativeAuthClientConfig(origin=wallet, api_url=api) 44 | client = NativeAuthClient(config) 45 | 46 | init_token = client.initialize() 47 | token_for_siginig = f"{account.address.to_bech32()}{init_token}" 48 | signature = account.sign_message(Message(token_for_siginig.encode())) 49 | 50 | access_token = client.get_token(address=account.address, token=init_token, signature=signature.hex()) 51 | 52 | logger.info(f"Requesting funds for address: {account.address.to_bech32()}") 53 | call_web_wallet_faucet(wallet_url=wallet, access_token=access_token) 54 | 55 | 56 | def call_web_wallet_faucet(wallet_url: str, access_token: str): 57 | faucet_url = f"{wallet_url}/faucet?accessToken={access_token}" 58 | webbrowser.open_new_tab(faucet_url) 59 | 60 | 61 | def get_wallet_and_api_urls(args: Any) -> tuple[str, str]: 62 | chain: str = args.chain 63 | 64 | if not chain: 65 | return get_custom_wallet_and_api_urls(args) 66 | 67 | if chain.upper() == "D": 68 | return WebWalletUrls.DEVNET.value, ApiUrls.DEVNET.value 69 | 70 | if chain.upper() == "T": 71 | return WebWalletUrls.TESTNET.value, ApiUrls.TESTNET.value 72 | 73 | raise BadUserInput("Invalid chain id. Choose between 'D' for devnet and 'T' for testnet.") 74 | 75 | 76 | def get_custom_wallet_and_api_urls(args: Any) -> tuple[str, str]: 77 | wallet = args.wallet_url 78 | api = args.api 79 | 80 | if not wallet: 81 | raise ArgumentsNotProvidedError("--wallet-url not provided") 82 | 83 | if not api: 84 | raise ArgumentsNotProvidedError("--api not provided") 85 | 86 | return wallet, api 87 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_ledger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any 3 | 4 | from multiversx_sdk import LedgerApp 5 | 6 | from multiversx_sdk_cli import cli_shared 7 | 8 | logger = logging.getLogger("cli.ledger") 9 | 10 | 11 | def setup_parser(subparsers: Any) -> Any: 12 | parser = cli_shared.add_group_subparser(subparsers, "ledger", "Get Ledger App addresses and version") 13 | subparsers = parser.add_subparsers() 14 | 15 | sub = cli_shared.add_command_subparser(subparsers, "ledger", "addresses", "Get the addresses within Ledger") 16 | sub.add_argument( 17 | "--num-addresses", 18 | required=False, 19 | type=int, 20 | default=10, 21 | help="The number of addresses to fetch", 22 | ) 23 | sub.set_defaults(func=print_addresses) 24 | 25 | sub = cli_shared.add_command_subparser( 26 | subparsers, 27 | "ledger", 28 | "version", 29 | "Get the version of the MultiversX App for Ledger", 30 | ) 31 | sub.set_defaults(func=print_version) 32 | 33 | return subparsers 34 | 35 | 36 | def print_addresses(args: Any): 37 | ledger_app = LedgerApp() 38 | for i in range(args.num_addresses): 39 | address = ledger_app.get_address(i) 40 | print("account index = %d | address index = %d | address: %s" % (0, i, address)) 41 | ledger_app.close() 42 | 43 | 44 | def print_version(args: Any): 45 | ledger_app = LedgerApp() 46 | print("MultiversX App version: " + ledger_app.get_version()) 47 | ledger_app.close() 48 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_localnet.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | from multiversx_sdk_cli import cli_shared, ux 6 | from multiversx_sdk_cli.constants import ONE_YEAR_IN_SECONDS 7 | from multiversx_sdk_cli.errors import KnownError 8 | from multiversx_sdk_cli.localnet import ( 9 | step_build_software, 10 | step_clean, 11 | step_config, 12 | step_new, 13 | step_prerequisites, 14 | step_start, 15 | ) 16 | 17 | logger = logging.getLogger("cli.localnet") 18 | 19 | 20 | def setup_parser(args: list[str], subparsers: Any) -> Any: 21 | parser = cli_shared.add_group_subparser(subparsers, "localnet", "Set up, start and control localnets") 22 | subparsers = parser.add_subparsers() 23 | 24 | # Setup 25 | sub = cli_shared.add_command_subparser( 26 | subparsers, 27 | "localnet", 28 | "setup", 29 | "Set up a localnet (runs 'prerequisites', 'build' and 'config' in one go)", 30 | ) 31 | add_argument_configfile(sub) 32 | sub.set_defaults(func=localnet_setup) 33 | 34 | # New 35 | sub = cli_shared.add_command_subparser(subparsers, "localnet", "new", "Create a new localnet configuration") 36 | add_argument_configfile(sub) 37 | sub.set_defaults(func=localnet_new) 38 | 39 | # Prerequisites 40 | sub = cli_shared.add_command_subparser( 41 | subparsers, 42 | "localnet", 43 | "prerequisites", 44 | "Download and verify the prerequisites for running a localnet", 45 | ) 46 | add_argument_configfile(sub) 47 | sub.set_defaults(func=localnet_prerequisites) 48 | 49 | # Build 50 | sub = cli_shared.add_command_subparser( 51 | subparsers, 52 | "localnet", 53 | "build", 54 | "Build necessary software for running a localnet", 55 | ) 56 | add_argument_configfile(sub) 57 | sub.add_argument( 58 | "--software", 59 | choices=["node", "seednode", "proxy"], 60 | nargs="+", 61 | default=["node", "seednode", "proxy"], 62 | help="The software to build (default: %(default)s)", 63 | ) 64 | sub.set_defaults(func=localnet_build) 65 | 66 | # Start 67 | sub = cli_shared.add_command_subparser(subparsers, "localnet", "start", "Start a localnet") 68 | add_argument_configfile(sub) 69 | sub.add_argument( 70 | "--stop-after-seconds", 71 | type=int, 72 | required=False, 73 | default=ONE_YEAR_IN_SECONDS, 74 | help="Stop the localnet after a given number of seconds (default: %(default)s)", 75 | ) 76 | sub.set_defaults(func=localnet_start) 77 | 78 | # Config 79 | sub = cli_shared.add_command_subparser( 80 | subparsers, 81 | "localnet", 82 | "config", 83 | "Configure a localnet (required before starting it the first time or after clean)", 84 | ) 85 | add_argument_configfile(sub) 86 | sub.set_defaults(func=localnet_config) 87 | 88 | # Clean 89 | sub = cli_shared.add_command_subparser( 90 | subparsers, 91 | "localnet", 92 | "clean", 93 | "Erase the currently configured localnet (must be already stopped)", 94 | ) 95 | add_argument_configfile(sub) 96 | sub.set_defaults(func=localnet_clean) 97 | 98 | 99 | def add_argument_configfile(parser: Any): 100 | help_config_file = "An optional configuration file describing the localnet" 101 | parser.add_argument( 102 | "--configfile", 103 | type=Path, 104 | required=False, 105 | default=Path("localnet.toml"), 106 | help=help_config_file, 107 | ) 108 | 109 | 110 | def localnet_new(args: Any): 111 | logger.info("New localnet (creating configuration file)...") 112 | 113 | step_new.new_config(args.configfile) 114 | 115 | ux.show_message( 116 | "New localnet configuration file created (or already existing). Make sure to inspect it. In order to fetch localnet prerequisites, run:\n\n$ mxpy localnet prerequisites" 117 | ) 118 | 119 | 120 | def localnet_clean(args: Any): 121 | logger.info("Cleaning localnet...") 122 | guard_configfile(args) 123 | 124 | step_clean.clean(configfile=args.configfile) 125 | 126 | ux.show_message("Localnet cleaned. In order to configure (prepare) the localnet, run:\n\n$ mxpy localnet config") 127 | 128 | 129 | def localnet_prerequisites(args: Any): 130 | logger.info("Gathering prerequisites...") 131 | guard_configfile(args) 132 | 133 | step_prerequisites.fetch_prerequisites(configfile=args.configfile) 134 | 135 | ux.show_message("Prerequisites gathered. In order to build the localnet software, run:\n\n$ mxpy localnet build") 136 | 137 | 138 | def localnet_build(args: Any): 139 | logger.info("Building binaries...") 140 | guard_configfile(args) 141 | 142 | step_build_software.build(configfile=args.configfile, software_components=args.software) 143 | 144 | ux.show_message("Binaries built. In order to configure (prepare) the localnet, run:\n\n$ mxpy localnet config") 145 | 146 | 147 | def localnet_config(args: Any): 148 | logger.info("Configuring localnet...") 149 | guard_configfile(args) 150 | 151 | step_config.configure(configfile=args.configfile) 152 | 153 | ux.show_message("Localnet configured. In order to start it, run:\n\n$ mxpy localnet start") 154 | 155 | 156 | def localnet_start(args: Any): 157 | logger.info("Starting localnet...") 158 | guard_configfile(args) 159 | 160 | step_start.start(configfile=args.configfile, stop_after_seconds=args.stop_after_seconds) 161 | 162 | 163 | def localnet_setup(args: Any): 164 | logger.info("Setting up localnet...") 165 | 166 | step_new.new_config(args.configfile) 167 | step_prerequisites.fetch_prerequisites(configfile=args.configfile) 168 | step_build_software.build(configfile=args.configfile, software_components=["node", "seednode", "proxy"]) 169 | step_clean.clean(configfile=args.configfile) 170 | step_config.configure(configfile=args.configfile) 171 | 172 | ux.show_message("Localnet setup complete. In order to start it, run:\n\n$ mxpy localnet start") 173 | 174 | 175 | def guard_configfile(args: Any): 176 | configfile = args.configfile.resolve() 177 | old_configfile_in_workdir = Path("testnet.toml").resolve() 178 | 179 | if not configfile.exists(): 180 | raise KnownError(f"Localnet config file does not exist: {configfile}") 181 | 182 | if old_configfile_in_workdir.exists(): 183 | logger.error( 184 | f"""For less ambiguity, the old "testnet.toml" config file should be removed: {old_configfile_in_workdir}""" 185 | ) 186 | raise KnownError(f"""Found old "testnet.toml" config file in working directory: {old_configfile_in_workdir}""") 187 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_output.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from collections import OrderedDict 4 | from typing import Any, Optional, Union 5 | 6 | from multiversx_sdk import Address, Transaction, TransactionOnNetwork 7 | 8 | from multiversx_sdk_cli import utils 9 | from multiversx_sdk_cli.utils import ISerializable 10 | 11 | logger = logging.getLogger("cli.output") 12 | 13 | 14 | class CLIOutputBuilder: 15 | def __init__(self) -> None: 16 | self.emitted_transaction_hash: Optional[str] = None 17 | self.emitted_transaction: Union[Transaction, None] = None 18 | self.emitted_transaction_omitted_fields: list[str] = [] 19 | self.contract_address: Union[Address, None] = None 20 | self.transaction_on_network: Union[TransactionOnNetwork, None] = None 21 | self.transaction_on_network_omitted_fields: list[str] = [] 22 | self.simulation_results: Union[ISerializable, None] = None 23 | 24 | def set_emitted_transaction_hash(self, hash: str): 25 | self.emitted_transaction_hash = hash 26 | return self 27 | 28 | def set_emitted_transaction(self, emitted_transaction: Transaction, omitted_fields: list[str] = []): 29 | self.emitted_transaction = emitted_transaction 30 | self.emitted_transaction_omitted_fields = omitted_fields 31 | return self 32 | 33 | def set_contract_address(self, contract_address: Address): 34 | self.contract_address = contract_address 35 | return self 36 | 37 | def set_awaited_transaction(self, awaited_transaction: TransactionOnNetwork, omitted_fields: list[str] = []): 38 | self.set_transaction_on_network(awaited_transaction, omitted_fields) 39 | self.set_emitted_transaction_hash(awaited_transaction.hash.hex()) 40 | 41 | def set_transaction_on_network( 42 | self, 43 | transaction_on_network: TransactionOnNetwork, 44 | omitted_fields: list[str] = [], 45 | ): 46 | self.transaction_on_network = transaction_on_network 47 | self.transaction_on_network_omitted_fields = omitted_fields 48 | return self 49 | 50 | def set_simulation_results(self, simulation_results: ISerializable): 51 | self.simulation_results = simulation_results 52 | return self 53 | 54 | def build(self) -> dict[str, Any]: 55 | output: dict[str, Any] = OrderedDict() 56 | 57 | if self.emitted_transaction: 58 | emitted_transaction_dict = self.emitted_transaction.to_dictionary() 59 | emitted_transaction_hash = self.emitted_transaction_hash or "" 60 | emitted_transaction_data = self.emitted_transaction.data.decode() 61 | utils.omit_fields(emitted_transaction_dict, self.emitted_transaction_omitted_fields) 62 | 63 | output["emittedTransaction"] = emitted_transaction_dict 64 | output["emittedTransactionData"] = emitted_transaction_data 65 | output["emittedTransactionHash"] = emitted_transaction_hash 66 | 67 | if self.contract_address: 68 | contract_address = self.contract_address.to_bech32() 69 | output["contractAddress"] = contract_address 70 | 71 | if self.transaction_on_network: 72 | transaction_on_network_dict = self.transaction_on_network.raw 73 | utils.omit_fields(transaction_on_network_dict, self.transaction_on_network_omitted_fields) 74 | output["transactionOnNetwork"] = transaction_on_network_dict 75 | 76 | if self.simulation_results: 77 | output["simulation"] = self.simulation_results 78 | 79 | return output 80 | 81 | @classmethod 82 | def describe( 83 | cls, 84 | with_emitted: bool = True, 85 | with_contract: bool = False, 86 | with_transaction_on_network: bool = False, 87 | with_simulation: bool = False, 88 | ) -> str: 89 | output: dict[str, Any] = OrderedDict() 90 | 91 | if with_emitted: 92 | output["emittedTransaction"] = { 93 | "nonce": 42, 94 | "sender": "alice", 95 | "receiver": "bob", 96 | "...": "...", 97 | } 98 | output["emittedTransactionData"] = "the transaction data, not encoded" 99 | output["emittedTransactionHash"] = "the transaction hash" 100 | 101 | if with_contract: 102 | output["contractAddress"] = "the address of the contract" 103 | 104 | if with_transaction_on_network: 105 | output["transactionOnNetwork"] = { 106 | "nonce": 42, 107 | "sender": "alice", 108 | "receiver": "bob", 109 | "...": "...", 110 | } 111 | 112 | if with_simulation: 113 | output["simulation"] = {"execution": {"...": "..."}, "cost": {"...": "..."}} 114 | 115 | description = json.dumps(output, indent=4) 116 | description_wrapped = f""" 117 | 118 | Output example: 119 | =============== 120 | {description} 121 | """ 122 | return description_wrapped 123 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_password.py: -------------------------------------------------------------------------------- 1 | from getpass import getpass 2 | from typing import Any 3 | 4 | 5 | def load_password(args: Any) -> str: 6 | if args.passfile: 7 | with open(args.passfile) as pass_file: 8 | return pass_file.read().strip() 9 | return getpass("Keyfile's password: ") 10 | 11 | 12 | def load_guardian_password(args: Any) -> str: 13 | if args.guardian_passfile: 14 | with open(args.guardian_passfile) as pass_file: 15 | return pass_file.read().strip() 16 | return getpass("Keyfile's password: ") 17 | 18 | 19 | def load_relayer_password(args: Any) -> str: 20 | if args.relayer_passfile: 21 | with open(args.relayer_passfile) as pass_file: 22 | return pass_file.read().strip() 23 | return getpass("Keyfile's password: ") 24 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cli_validator_wallet.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | from multiversx_sdk import ValidatorPEM, ValidatorSecretKey, ValidatorSigner 6 | 7 | from multiversx_sdk_cli import cli_shared, utils 8 | from multiversx_sdk_cli.errors import BadUserInput 9 | from multiversx_sdk_cli.sign_verify import SignedMessage, sign_message_by_validator 10 | from multiversx_sdk_cli.ux import show_critical_error, show_message 11 | 12 | logger = logging.getLogger("cli.validator_wallet") 13 | 14 | 15 | def setup_parser(args: list[str], subparsers: Any) -> Any: 16 | parser = cli_shared.add_group_subparser( 17 | subparsers, 18 | "validator-wallet", 19 | "Create a validator wallet, sign and verify messages and convert a validator wallet to a hex secret key.", 20 | ) 21 | subparsers = parser.add_subparsers() 22 | 23 | sub = cli_shared.add_command_subparser( 24 | subparsers, 25 | "validator-wallet", 26 | "new", 27 | "Create a new validator wallet and save it as a PEM file.", 28 | ) 29 | sub.add_argument( 30 | "--outfile", 31 | help="the output path and file name for the generated wallet", 32 | type=str, 33 | required=True, 34 | ) 35 | sub.set_defaults(func=create_new_wallet) 36 | 37 | sub = cli_shared.add_command_subparser(subparsers, "validator-wallet", "sign-message", "Sign a message.") 38 | sub.add_argument("--message", required=True, help="the message you want to sign") 39 | sub.add_argument("--pem", required=True, type=str, help="the path to a validator pem file") 40 | sub.add_argument( 41 | "--index", 42 | required=False, 43 | type=int, 44 | default=0, 45 | help="the index of the validator in case the file contains multiple validators (default: %(default)s)", 46 | ) 47 | sub.set_defaults(func=sign_message) 48 | 49 | sub = cli_shared.add_command_subparser( 50 | subparsers, "validator-wallet", "verify-message-signature", "Verify a previously signed message." 51 | ) 52 | sub.add_argument("--pubkey", required=True, help="the hex string representing the validator's public key") 53 | sub.add_argument( 54 | "--message", 55 | required=True, 56 | help="the previously signed message(readable text, as it was signed)", 57 | ) 58 | sub.add_argument("--signature", required=True, help="the signature in hex format") 59 | sub.set_defaults(func=verify_message_signature) 60 | 61 | sub = cli_shared.add_command_subparser( 62 | subparsers, "validator-wallet", "convert", "Convert a validator pem file to a hex secret key." 63 | ) 64 | sub.add_argument("--infile", required=True, help="the pem file of the wallet") 65 | sub.add_argument( 66 | "--index", 67 | required=False, 68 | type=int, 69 | default=0, 70 | help="the index of the validator in case the file contains multiple validators (default: %(default)s)", 71 | ) 72 | sub.set_defaults(func=convert_wallet_to_secret_key) 73 | 74 | parser.epilog = cli_shared.build_group_epilog(subparsers) 75 | return subparsers 76 | 77 | 78 | def create_new_wallet(args: Any): 79 | path = Path(args.outfile).expanduser().resolve() 80 | 81 | if path.exists(): 82 | raise BadUserInput(f"File already exists, will not overwrite: {str(path)}") 83 | 84 | secret_key = ValidatorSecretKey.generate() 85 | public_key = secret_key.generate_public_key() 86 | validator_pem = ValidatorPEM(label=public_key.hex(), secret_key=secret_key) 87 | validator_pem.save(path) 88 | 89 | logger.info(f"Validator wallet saved: {str(path)}") 90 | 91 | 92 | def sign_message(args: Any): 93 | path = Path(args.pem).expanduser().resolve() 94 | validator_signer = ValidatorSigner.from_pem_file(path, args.index) 95 | signed_message = sign_message_by_validator(args.message, validator_signer) 96 | 97 | utils.dump_out_json(signed_message.to_dictionary()) 98 | 99 | 100 | def verify_message_signature(args: Any): 101 | message = args.message 102 | pubkey = args.pubkey 103 | 104 | signed_message = SignedMessage(pubkey, message, args.signature) 105 | is_signed = signed_message.verify_validator_signature() 106 | 107 | if is_signed: 108 | show_message(f"""SUCCESS: The message "{message}" was signed by {pubkey}""") 109 | else: 110 | show_critical_error(f"""FAILED: The message "{message}" was NOT signed by {pubkey}""") 111 | 112 | 113 | def convert_wallet_to_secret_key(args: Any): 114 | file = args.infile 115 | index = args.index 116 | 117 | path = Path(file).expanduser().resolve() 118 | if not path.is_file(): 119 | raise BadUserInput("File not found") 120 | 121 | signer = ValidatorSigner.from_pem_file(path, index) 122 | print(f"Public key: {signer.get_pubkey().hex()}") 123 | print(f"Secret key: {signer.secret_key.hex()}") 124 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/constants.py: -------------------------------------------------------------------------------- 1 | VM_TYPE_SYSTEM = "0001" 2 | VM_TYPE_WASM_VM = "0500" 3 | SC_HEX_PUBKEY_PREFIX = "0" * 16 4 | SC_HEX_PUBKEY_PREFIX_SYSTEM = SC_HEX_PUBKEY_PREFIX + VM_TYPE_SYSTEM + "0" * 30 5 | SC_HEX_PUBKEY_PREFIX_WASM_VM = SC_HEX_PUBKEY_PREFIX + VM_TYPE_WASM_VM 6 | DEFAULT_CARGO_TARGET_DIR_NAME = "default_cargo_target" 7 | TRANSACTION_OPTIONS_TX_GUARDED = 0b0010 8 | ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365 9 | 10 | DEFAULT_TX_VERSION = 2 11 | 12 | ADDRESS_ZERO_HEX = "0000000000000000000000000000000000000000000000000000000000000000" 13 | 14 | NUMBER_OF_SHARDS = 3 15 | 16 | DEFAULT_GAS_PRICE = 1000000000 17 | MIN_GAS_LIMIT = 50000 18 | 19 | TCS_SERVICE_ID = "MultiversXTCSService" 20 | EXTRA_GAS_LIMIT_FOR_GUARDED_TRANSACTIONS = 50_000 21 | EXTRA_GAS_LIMIT_FOR_RELAYED_TRANSACTIONS = 50_000 22 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/contract_verification.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import json 3 | import logging 4 | import time 5 | from pathlib import Path 6 | from typing import Any, Optional, Protocol 7 | 8 | import requests 9 | from multiversx_sdk import Address, Message 10 | 11 | from multiversx_sdk_cli.errors import KnownError 12 | from multiversx_sdk_cli.utils import dump_out_json, read_json_file 13 | 14 | HTTP_REQUEST_TIMEOUT = 408 15 | HTTP_SUCCESS = 200 16 | 17 | logger = logging.getLogger("cli.contracts.verifier") 18 | 19 | 20 | # fmt: off 21 | class IAccount(Protocol): 22 | def sign_message(self, message: Message) -> bytes: 23 | ... 24 | # fmt: on 25 | 26 | 27 | class ContractVerificationRequest: 28 | def __init__( 29 | self, 30 | contract: Address, 31 | source_code: dict[str, Any], 32 | signature: bytes, 33 | docker_image: str, 34 | contract_variant: Optional[str], 35 | ) -> None: 36 | self.contract = contract 37 | self.source_code = source_code 38 | self.signature = signature 39 | self.docker_image = docker_image 40 | self.contract_variant = contract_variant 41 | 42 | def to_dictionary(self) -> dict[str, Any]: 43 | return { 44 | "signature": self.signature.hex(), 45 | "payload": { 46 | "contract": self.contract.bech32(), 47 | "dockerImage": self.docker_image, 48 | "sourceCode": self.source_code, 49 | "contractVariant": self.contract_variant, 50 | }, 51 | } 52 | 53 | 54 | class ContractVerificationPayload: 55 | def __init__( 56 | self, 57 | contract: Address, 58 | source_code: dict[str, Any], 59 | docker_image: str, 60 | contract_variant: Optional[str], 61 | ) -> None: 62 | self.contract = contract 63 | self.source_code = source_code 64 | self.docker_image = docker_image 65 | self.contract_variant = contract_variant 66 | 67 | def serialize(self) -> str: 68 | payload = { 69 | "contract": self.contract.to_bech32(), 70 | "dockerImage": self.docker_image, 71 | "sourceCode": self.source_code, 72 | "contractVariant": self.contract_variant, 73 | } 74 | 75 | return json.dumps(payload, separators=(",", ":")) 76 | 77 | 78 | def trigger_contract_verification( 79 | packaged_source: Path, 80 | owner: IAccount, 81 | contract: Address, 82 | verifier_url: str, 83 | docker_image: str, 84 | contract_variant: Optional[str], 85 | ): 86 | source_code = read_json_file(packaged_source) 87 | 88 | payload = ContractVerificationPayload(contract, source_code, docker_image, contract_variant).serialize() 89 | signature = _create_request_signature(owner, contract, payload.encode()) 90 | contract_verification = ContractVerificationRequest( 91 | contract, source_code, signature, docker_image, contract_variant 92 | ) 93 | 94 | request_dictionary = contract_verification.to_dictionary() 95 | 96 | url = f"{verifier_url}/verifier" 97 | status_code, message, data = _do_post(url, request_dictionary) 98 | 99 | if status_code == HTTP_REQUEST_TIMEOUT: 100 | task_id = data.get("taskId", "") 101 | 102 | if task_id: 103 | query_status_with_task_id(verifier_url, task_id) 104 | else: 105 | dump_out_json(data) 106 | elif status_code != HTTP_SUCCESS: 107 | dump_out_json(data) 108 | raise KnownError(f"Cannot verify contract: {message}") 109 | else: 110 | status = data.get("status", "") 111 | if status: 112 | logger.info(f"Task status: {status}") 113 | dump_out_json(data) 114 | else: 115 | task_id = data.get("taskId", "") 116 | query_status_with_task_id(verifier_url, task_id) 117 | 118 | 119 | def _create_request_signature(account: IAccount, contract_address: Address, request_payload: bytes) -> bytes: 120 | hashed_payload: str = hashlib.sha256(request_payload).hexdigest() 121 | raw_data_to_sign = f"{contract_address.to_bech32()}{hashed_payload}" 122 | 123 | return account.sign_message(Message(raw_data_to_sign.encode())) 124 | 125 | 126 | def query_status_with_task_id(url: str, task_id: str, interval: int = 10): 127 | logger.info("Please wait while we verify your contract. This may take a while.") 128 | old_status = "" 129 | 130 | while True: 131 | _, _, response = _do_get(f"{url}/tasks/{task_id}") 132 | status = response.get("status", "") 133 | 134 | if status == "finished": 135 | logger.info("Verification finished!") 136 | dump_out_json(response) 137 | break 138 | elif status != old_status: 139 | logger.info(f"Task status: {status}") 140 | dump_out_json(response) 141 | old_status = status 142 | 143 | time.sleep(interval) 144 | 145 | 146 | def _do_post(url: str, payload: Any) -> tuple[int, str, dict[str, Any]]: 147 | logger.debug(f"_do_post() to {url}") 148 | response = requests.post(url, json=payload) 149 | 150 | try: 151 | data = response.json() 152 | message = data.get("message", "") 153 | return response.status_code, message, data 154 | except Exception as error: 155 | logger.error(f"Erroneous response from {url}: {response.text}") 156 | raise KnownError(f"Cannot parse response from {url}", error) 157 | 158 | 159 | def _do_get(url: str) -> tuple[int, str, dict[str, Any]]: 160 | logger.debug(f"_do_get() from {url}") 161 | response = requests.get(url) 162 | 163 | try: 164 | data = response.json() 165 | message = data.get("message", "") 166 | return response.status_code, message, data 167 | except Exception as error: 168 | logger.error(f"Erroneous response from {url}: {response.text}") 169 | raise KnownError(f"Cannot parse response from {url}", error) 170 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/cosign_transaction.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | import requests 4 | from multiversx_sdk import Transaction 5 | 6 | from multiversx_sdk_cli.errors import GuardianServiceError 7 | 8 | 9 | def cosign_transaction(transaction: Transaction, service_url: str, guardian_code: str): 10 | payload = { 11 | "code": f"{guardian_code}", 12 | "transactions": [transaction.to_dictionary()], 13 | } 14 | 15 | # we call sign-multiple-transactions to be allowed a bigger payload (e.g. deploying large contracts) 16 | url = f"{service_url}/sign-multiple-transactions" 17 | response = requests.post(url, json=payload) 18 | check_for_guardian_error(response.json()) 19 | 20 | # we only send 1 transaction 21 | tx_as_dict = response.json()["data"]["transactions"][0] 22 | transaction.guardian_signature = bytes.fromhex(tx_as_dict["guardianSignature"]) 23 | 24 | 25 | def check_for_guardian_error(response: dict[str, Any]): 26 | error = response["error"] 27 | 28 | if error: 29 | raise GuardianServiceError(error) 30 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/dependencies/__init__.py: -------------------------------------------------------------------------------- 1 | from multiversx_sdk_cli.dependencies.install import ( 2 | get_all_deps, 3 | get_golang, 4 | get_module_by_key, 5 | get_module_directory, 6 | install_module, 7 | ) 8 | 9 | __all__ = [ 10 | "install_module", 11 | "get_module_directory", 12 | "get_module_by_key", 13 | "get_golang", 14 | "get_all_deps", 15 | ] 16 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/dependencies/install.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | from typing import Dict, List 4 | 5 | from multiversx_sdk_cli import config, errors 6 | from multiversx_sdk_cli.dependencies.modules import ( 7 | DependencyModule, 8 | GolangModule, 9 | TestWalletsModule, 10 | ) 11 | 12 | logger = logging.getLogger("install") 13 | 14 | 15 | def install_module(key: str, overwrite: bool = False): 16 | if key == "all": 17 | modules = get_all_deps() 18 | else: 19 | modules = [get_module_by_key(key)] 20 | 21 | for module in modules: 22 | module.install(overwrite) 23 | 24 | 25 | def get_module_directory(key: str) -> Path: 26 | module = get_module_by_key(key) 27 | default_tag = config.get_dependency_tag(key) 28 | directory = module.get_directory(default_tag) 29 | return directory 30 | 31 | 32 | def get_module_by_key(key: str) -> DependencyModule: 33 | matches = [module for module in get_all_deps() if module.key == key or key in module.aliases] 34 | if len(matches) != 1: 35 | raise errors.UnknownDependency(key) 36 | 37 | return matches[0] 38 | 39 | 40 | def get_deps_dict() -> Dict[str, DependencyModule]: 41 | deps: Dict[str, DependencyModule] = dict() 42 | 43 | for module in get_all_deps(): 44 | deps[module.key] = module 45 | for alias in module.aliases: 46 | deps[alias] = module 47 | 48 | return deps 49 | 50 | 51 | def get_all_deps() -> List[DependencyModule]: 52 | return [ 53 | GolangModule(key="golang"), 54 | TestWalletsModule(key="testwallets"), 55 | ] 56 | 57 | 58 | def get_golang() -> GolangModule: 59 | golang = get_module_by_key("golang") 60 | assert isinstance(golang, GolangModule) 61 | return golang 62 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/dependencies/resolution.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | from multiversx_sdk_cli import config 4 | 5 | 6 | class DependencyResolution(Enum): 7 | SDK = "SDK" 8 | Host = "Host" 9 | 10 | 11 | def get_dependency_resolution(dependency: str) -> DependencyResolution: 12 | value = config.get_dependency_resolution(dependency) 13 | 14 | if value.lower() == "host": 15 | return DependencyResolution.Host 16 | if value.lower() == "sdk": 17 | return DependencyResolution.SDK 18 | 19 | return DependencyResolution.SDK 20 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/dns.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Protocol 2 | 3 | from Cryptodome.Hash import keccak 4 | from multiversx_sdk import ( 5 | Address, 6 | AddressComputer, 7 | SmartContractQuery, 8 | SmartContractQueryResponse, 9 | ) 10 | 11 | from multiversx_sdk_cli import cli_shared 12 | from multiversx_sdk_cli.args_validation import ( 13 | ensure_wallet_args_are_provided, 14 | validate_broadcast_args, 15 | validate_chain_id_args, 16 | validate_transaction_args, 17 | ) 18 | from multiversx_sdk_cli.config import get_address_hrp 19 | from multiversx_sdk_cli.constants import ADDRESS_ZERO_HEX 20 | from multiversx_sdk_cli.transactions import TransactionsController 21 | 22 | MaxNumShards = 256 23 | ShardIdentiferLen = 2 24 | InitialDNSAddress = bytes([1] * 32) 25 | 26 | 27 | # fmt: off 28 | class INetworkProvider(Protocol): 29 | def query_contract(self, query: SmartContractQuery) -> SmartContractQueryResponse: 30 | ... 31 | # fmt: on 32 | 33 | 34 | def resolve(name: str, proxy: INetworkProvider) -> Address: 35 | dns_address = dns_address_for_name(name) 36 | 37 | response = _query_contract(contract_address=dns_address, proxy=proxy, function="resolve", args=[name.encode()]) 38 | 39 | if len(response.return_data_parts) == 0: 40 | return Address.new_from_hex(ADDRESS_ZERO_HEX, get_address_hrp()) 41 | 42 | result = response.return_data_parts[0] 43 | return Address(result, get_address_hrp()) 44 | 45 | 46 | def validate_name(name: str, shard_id: int, proxy: INetworkProvider): 47 | dns_address = compute_dns_address_for_shard_id(shard_id) 48 | 49 | response = _query_contract( 50 | contract_address=dns_address, 51 | proxy=proxy, 52 | function="validateName", 53 | args=[name.encode()], 54 | ) 55 | 56 | return_code: str = response.return_code 57 | if return_code == "ok": 58 | print(f"name [{name}] is valid") 59 | else: 60 | print(f"name [{name}] is invalid") 61 | 62 | 63 | def register(args: Any): 64 | validate_transaction_args(args) 65 | ensure_wallet_args_are_provided(args) 66 | validate_broadcast_args(args) 67 | validate_chain_id_args(args) 68 | 69 | sender = cli_shared.prepare_sender(args) 70 | guardian_and_relayer_data = cli_shared.get_guardian_and_relayer_data( 71 | sender=sender.address.to_bech32(), 72 | args=args, 73 | ) 74 | 75 | native_amount = int(args.value) 76 | 77 | receiver = dns_address_for_name(args.name) 78 | data = dns_register_data(args.name) 79 | 80 | chain_id = cli_shared.get_chain_id(args.chain, args.proxy) 81 | controller = TransactionsController(chain_id) 82 | 83 | tx = controller.create_transaction( 84 | sender=sender, 85 | receiver=receiver, 86 | native_amount=native_amount, 87 | gas_limit=args.gas_limit, 88 | gas_price=args.gas_price, 89 | nonce=sender.nonce, 90 | version=args.version, 91 | options=args.options, 92 | data=data, 93 | guardian_and_relayer_data=guardian_and_relayer_data, 94 | ) 95 | 96 | cli_shared.send_or_simulate(tx, args) 97 | 98 | 99 | def compute_all_dns_addresses() -> list[Address]: 100 | addresses: list[Address] = [] 101 | for i in range(0, 256): 102 | addresses.append(compute_dns_address_for_shard_id(i)) 103 | return addresses 104 | 105 | 106 | def name_hash(name: str) -> bytes: 107 | return keccak.new(digest_bits=256).update(str.encode(name)).digest() 108 | 109 | 110 | def registration_cost(shard_id: int, proxy: INetworkProvider) -> int: 111 | dns_address = compute_dns_address_for_shard_id(shard_id) 112 | 113 | response = _query_contract( 114 | contract_address=dns_address, 115 | proxy=proxy, 116 | function="getRegistrationCost", 117 | args=[], 118 | ) 119 | 120 | data = response.return_data_parts[0] 121 | if not data: 122 | return 0 123 | else: 124 | return int.from_bytes(data, byteorder="big", signed=False) 125 | 126 | 127 | def version(shard_id: int, proxy: INetworkProvider) -> str: 128 | dns_address = compute_dns_address_for_shard_id(shard_id) 129 | 130 | response = _query_contract(contract_address=dns_address, proxy=proxy, function="version", args=[]) 131 | return response.return_data_parts[0].decode() 132 | 133 | 134 | def dns_address_for_name(name: str) -> Address: 135 | hash = name_hash(name) 136 | shard_id = hash[31] 137 | return compute_dns_address_for_shard_id(shard_id) 138 | 139 | 140 | def compute_dns_address_for_shard_id(shard_id: int) -> Address: 141 | deployer_pubkey_prefix = InitialDNSAddress[: len(InitialDNSAddress) - ShardIdentiferLen] 142 | 143 | deployer_pubkey = deployer_pubkey_prefix + bytes([0, shard_id]) 144 | deployer = Address(deployer_pubkey, get_address_hrp()) 145 | nonce = 0 146 | address_computer = AddressComputer(number_of_shards=3) 147 | contract_address = address_computer.compute_contract_address(deployer, nonce) 148 | return contract_address 149 | 150 | 151 | def dns_register_data(name: str) -> str: 152 | name_as_hex = str.encode(name).hex() 153 | return f"register@{name_as_hex}" 154 | 155 | 156 | def _query_contract( 157 | contract_address: Address, 158 | proxy: INetworkProvider, 159 | function: str, 160 | args: list[bytes], 161 | ) -> SmartContractQueryResponse: 162 | query = SmartContractQuery(contract=contract_address, function=function, arguments=args) 163 | return proxy.query_contract(query) 164 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/docker.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import subprocess 4 | from pathlib import Path 5 | 6 | from multiversx_sdk_cli.errors import KnownError 7 | 8 | logger = logging.getLogger("docker") 9 | 10 | 11 | def is_docker_installed(): 12 | try: 13 | result = subprocess.run(["docker", "--version"], capture_output=True, text=True) 14 | output = result.stdout 15 | 16 | if "Docker version" in output: 17 | return True 18 | else: 19 | return False 20 | except Exception: 21 | logger.error("Something went wrong when checking if docker is installed!") 22 | 23 | 24 | def run_docker( 25 | image: str, 26 | project_path: Path, 27 | contract: str, 28 | output_path: Path, 29 | no_wasm_opt: bool, 30 | docker_interactive: bool, 31 | docker_tty: bool, 32 | no_default_platform: bool, 33 | ): 34 | docker_mount_args: list[str] = ["--volume", f"{output_path}:/output"] 35 | 36 | if project_path: 37 | docker_mount_args.extend(["--volume", f"{project_path}:/project"]) 38 | 39 | docker_args = ["docker", "run"] 40 | 41 | if not (no_default_platform): 42 | docker_args += ["--platform", "linux/amd64"] 43 | 44 | if docker_interactive: 45 | docker_args += ["--interactive"] 46 | if docker_tty: 47 | docker_args += ["--tty"] 48 | 49 | docker_args += docker_mount_args 50 | docker_args += ["--user", f"{str(os.getuid())}:{str(os.getgid())}"] 51 | docker_args += ["--rm", image] 52 | 53 | entrypoint_args: list[str] = [] 54 | 55 | if project_path: 56 | entrypoint_args.extend(["--project", "project"]) 57 | 58 | if no_wasm_opt: 59 | entrypoint_args.append("--no-wasm-opt") 60 | 61 | if contract: 62 | entrypoint_args.extend(["--contract", contract]) 63 | 64 | args = docker_args + entrypoint_args 65 | 66 | logger.info(f"Docker running with args: {args}") 67 | 68 | try: 69 | result = subprocess.run(args) 70 | result.check_returncode() 71 | except subprocess.CalledProcessError as error: 72 | raise KnownError(f"Docker run failed with error code: {error.returncode}") 73 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/downloader.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import sys 3 | 4 | import requests 5 | 6 | from multiversx_sdk_cli import errors 7 | 8 | logger = logging.getLogger("downloader") 9 | 10 | CHUNK_SIZE = 1024 * 16 11 | LINECLEAR = "\r" + " " * 20 + "\r" 12 | 13 | PROGRESS_RULER = "Downloading...\n|_,_,_,_,_,,_,_,_,_,_|" 14 | 15 | 16 | def download(url: str, filename: str) -> None: 17 | if not url: 18 | raise errors.BadUrlError("") 19 | 20 | logger.info(f"download_url.url: {url}") 21 | logger.info(f"download_url.filename: {filename}") 22 | 23 | try: 24 | response = requests.get(url, stream=True) 25 | response.raise_for_status() 26 | print(PROGRESS_RULER, file=sys.stderr) 27 | print(" ", end="", file=sys.stderr) 28 | sys.stderr.flush() 29 | total_size = int(response.headers.get("content-length", 0)) 30 | chunk_number = 0 31 | progress = 0 32 | with open(filename, "wb") as file: 33 | for chunk in response.iter_content(chunk_size=CHUNK_SIZE): 34 | file.write(chunk) 35 | progress = _report_download_progress(progress, chunk_number, total_size) 36 | chunk_number += 1 37 | file.flush() 38 | print("", file=sys.stderr) 39 | sys.stderr.flush() 40 | except requests.HTTPError as err: 41 | raise errors.DownloadError(f"Could not download [{url}] to [{filename}]") from err 42 | 43 | logger.info("Download done.") 44 | 45 | 46 | def _report_download_progress(progress: int, chunk_number: int, total_size: int): 47 | try: 48 | num_chunks = int(total_size / CHUNK_SIZE) 49 | new_progress = int((chunk_number / num_chunks) * 20) 50 | if new_progress > progress: 51 | progress_markers = "·" * (new_progress - progress) 52 | print(progress_markers, end="", file=sys.stderr) 53 | sys.stderr.flush() 54 | return new_progress 55 | except ZeroDivisionError: 56 | return 0 57 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/errors.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Union 2 | 3 | 4 | class KnownError(Exception): 5 | inner = None 6 | 7 | def __init__(self, message: str, inner: Union[Any, None] = None): 8 | super().__init__(message) 9 | self.inner = inner 10 | 11 | def get_pretty(self) -> str: 12 | if self.inner: 13 | return f"""{self} 14 | ... {self.inner} 15 | """ 16 | return str(self) 17 | 18 | 19 | class ProgrammingError(KnownError): 20 | pass 21 | 22 | 23 | class DownloadError(KnownError): 24 | pass 25 | 26 | 27 | class BadUrlError(DownloadError): 28 | pass 29 | 30 | 31 | class UnknownArchiveType(KnownError): 32 | pass 33 | 34 | 35 | class DependencyMissing(KnownError): 36 | def __init__(self, name: str, tag: str): 37 | super().__init__(f"Dependency missing: {name} {tag}") 38 | 39 | 40 | class DependenciesMissing(KnownError): 41 | def __init__(self, dependencies: list[tuple[str, str]]): 42 | message = "Dependencies missing: \n" 43 | 44 | for dependency in dependencies: 45 | message += f"{dependency[0]} {dependency[1]}\n" 46 | 47 | super().__init__(message.rstrip("\n")) 48 | 49 | 50 | class UnknownDependency(KnownError): 51 | def __init__(self, name: str): 52 | super().__init__(f"Unknown dependency: {name}") 53 | 54 | 55 | class BadDependencyResolution(ProgrammingError): 56 | def __init__(self, dependency: str, resolution: Any): 57 | super().__init__(f"Bad dependency resolution for {dependency}: {resolution}") 58 | 59 | 60 | class BadDirectory(KnownError): 61 | def __init__(self, directory: str): 62 | super().__init__(f"Bad directory: {directory}") 63 | 64 | 65 | class PlatformNotSupported(KnownError): 66 | def __init__(self, action_or_item: str, platform: str): 67 | super().__init__(f"[{action_or_item}] is not supported on platform [{platform}].") 68 | 69 | 70 | class BadInputError(KnownError): 71 | def __init__(self, input: str, message: str): 72 | super().__init__(f"Bad input [{input}]: {message}") 73 | 74 | 75 | class ExternalProcessError(KnownError): 76 | def __init__(self, command_line: str, message: str): 77 | super().__init__( 78 | f"""External process error: 79 | Command line: {command_line} 80 | Output: {message}""" 81 | ) 82 | 83 | 84 | class UnknownConfigurationError(KnownError): 85 | def __init__(self, name: str): 86 | super().__init__(f"Configuration entry is not known: {name}.") 87 | 88 | 89 | class ConfigurationShouldBeUniqueError(KnownError): 90 | def __init__(self, name: str): 91 | super().__init__(f"Configuration entry already exists: {name}.") 92 | 93 | 94 | class ConfigurationProtectedError(KnownError): 95 | def __init__(self, name: str): 96 | super().__init__(f"This configuration name is protected: {name}.") 97 | 98 | 99 | class BadUserInput(KnownError): 100 | def __init__(self, message: str): 101 | super().__init__(f"Bad user input: {message}.") 102 | 103 | 104 | class BadUsage(KnownError): 105 | def __init__(self, message: str): 106 | super().__init__(f"Bad usage: {message}.") 107 | 108 | 109 | class TransactionIsNotSigned(KnownError): 110 | def __init__(self): 111 | super().__init__("Transaction is not signed.") 112 | 113 | 114 | class NoWalletProvided(KnownError): 115 | def __init__(self): 116 | super().__init__("No wallet provided.") 117 | 118 | 119 | class DockerMissingError(KnownError): 120 | def __init__(self): 121 | super().__init__("Docker is not installed! Please visit https://docs.docker.com/get-docker/ to install docker.") 122 | 123 | 124 | class GuardianServiceError(KnownError): 125 | def __init__(self, message: str): 126 | super().__init__(message) 127 | 128 | 129 | class ArgumentsNotProvidedError(KnownError): 130 | def __init__(self, message: str): 131 | super().__init__(message) 132 | 133 | 134 | class WalletGenerationError(KnownError): 135 | def __init__(self, message: str): 136 | super().__init__(message) 137 | 138 | 139 | class QueryContractError(KnownError): 140 | def __init__(self, message: str, inner: Any = None): 141 | super().__init__(message, str(inner)) 142 | 143 | 144 | class IncorrectWalletError(KnownError): 145 | def __init__(self, message: str): 146 | super().__init__(message) 147 | 148 | 149 | class InvalidArgumentsError(KnownError): 150 | def __init__(self, message: str): 151 | super().__init__(message) 152 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/guardian_relayer_data.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from typing import Optional 3 | 4 | from multiversx_sdk import Address 5 | 6 | from multiversx_sdk_cli.interfaces import IAccount 7 | 8 | 9 | @dataclass 10 | class GuardianRelayerData: 11 | guardian: Optional[IAccount] = None 12 | guardian_address: Optional[Address] = None 13 | guardian_service_url: Optional[str] = None 14 | guardian_2fa_code: Optional[str] = None 15 | relayer: Optional[IAccount] = None 16 | relayer_address: Optional[Address] = None 17 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/guards.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from multiversx_sdk_cli import errors 4 | 5 | 6 | def is_file(input: Path): 7 | if not input.is_file(): 8 | raise errors.BadInputError(str(input), "is not a valid file") 9 | 10 | 11 | def is_directory(directory: Path): 12 | if not directory.is_dir(): 13 | raise errors.BadDirectory(str(directory)) 14 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/interfaces.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Protocol 2 | 3 | from multiversx_sdk import Address, Transaction 4 | 5 | 6 | # fmt: off 7 | class IAccount(Protocol): 8 | address: Address 9 | 10 | def sign_transaction(self, transaction: Transaction) -> bytes: 11 | ... 12 | 13 | 14 | class ISimulateResponse(Protocol): 15 | def to_dictionary(self) -> dict[str, Any]: 16 | ... 17 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-py-cli/e2da34bd5cb490ad4d99af4dfcffa54e44c1d4af/multiversx_sdk_cli/localnet/__init__.py -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/config_default.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from multiversx_sdk_cli.localnet.config_general import General 4 | from multiversx_sdk_cli.localnet.config_networking import Networking 5 | from multiversx_sdk_cli.localnet.config_sharding import Metashard, RegularShards 6 | from multiversx_sdk_cli.localnet.config_software import ( 7 | Software, 8 | SoftwareChainGo, 9 | SoftwareChainProxyGo, 10 | SoftwareResolution, 11 | ) 12 | 13 | general = General( 14 | log_level="*:DEBUG", 15 | genesis_delay_seconds=10, 16 | rounds_per_epoch=100, 17 | round_duration_milliseconds=6000, 18 | ) 19 | 20 | software = Software( 21 | mx_chain_go=SoftwareChainGo( 22 | resolution=SoftwareResolution.Remote, 23 | archive_url="https://github.com/multiversx/mx-chain-go/archive/refs/heads/master.zip", 24 | archive_download_folder=Path("~/multiversx-sdk") / "localnet_software_remote" / "downloaded" / "mx-chain-go", 25 | archive_extraction_folder=Path("~/multiversx-sdk") / "localnet_software_remote" / "extracted" / "mx-chain-go", 26 | local_path=Path("~/multiversx-sdk") / "localnet_software_local" / "mx-chain-go", 27 | ), 28 | mx_chain_proxy_go=SoftwareChainProxyGo( 29 | resolution=SoftwareResolution.Remote, 30 | archive_url="https://github.com/multiversx/mx-chain-proxy-go/archive/refs/heads/master.zip", 31 | archive_download_folder=Path("~/multiversx-sdk") 32 | / "localnet_software_remote" 33 | / "downloaded" 34 | / "mx-chain-proxy-go", 35 | archive_extraction_folder=Path("~/multiversx-sdk") 36 | / "localnet_software_remote" 37 | / "extracted" 38 | / "mx-chain-proxy-go", 39 | local_path=Path("~/multiversx-sdk") / "localnet_software_local" / "mx-chain-proxy-go", 40 | ), 41 | ) 42 | 43 | metashard = Metashard( 44 | consensus_size=1, 45 | num_observers=0, 46 | num_validators=1, 47 | ) 48 | 49 | shards = RegularShards( 50 | num_shards=2, 51 | consensus_size=1, 52 | num_observers_per_shard=0, 53 | num_validators_per_shard=1, 54 | ) 55 | 56 | networking = Networking( 57 | host="127.0.0.1", 58 | port_seednode=9999, 59 | port_seednode_rest_api=10000, 60 | p2p_id_seednode="16Uiu2HAkx4QqgXXDdHdUWbLu5kxhd3Uo2hqB2FfCxmxH5Sd7bZFk", 61 | port_proxy=7950, 62 | port_first_observer=21100, 63 | port_first_observer_rest_api=10100, 64 | port_first_validator=21500, 65 | port_first_validator_rest_api=10200, 66 | ) 67 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/config_general.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from multiversx_sdk_cli.localnet.config_part import ConfigPart 4 | 5 | 6 | class General(ConfigPart): 7 | def __init__( 8 | self, 9 | log_level: str, 10 | genesis_delay_seconds: int, 11 | rounds_per_epoch: int, 12 | round_duration_milliseconds: int, 13 | ): 14 | self.log_level: str = log_level 15 | self.genesis_delay_seconds: int = genesis_delay_seconds 16 | self.rounds_per_epoch: int = rounds_per_epoch 17 | self.round_duration_milliseconds: int = round_duration_milliseconds 18 | 19 | def get_name(self) -> str: 20 | return "general" 21 | 22 | def _do_override(self, other: Dict[str, Any]): 23 | self.log_level = other.get("log_level", self.log_level) 24 | self.genesis_delay_seconds = other.get("genesis_delay_seconds", self.genesis_delay_seconds) 25 | self.rounds_per_epoch = other.get("rounds_per_epoch", self.rounds_per_epoch) 26 | self.round_duration_milliseconds = other.get("round_duration_milliseconds", self.round_duration_milliseconds) 27 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/config_networking.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from multiversx_sdk_cli.localnet.config_part import ConfigPart 4 | 5 | 6 | class Networking(ConfigPart): 7 | def __init__( 8 | self, 9 | host: str, 10 | port_seednode: int, 11 | port_seednode_rest_api: int, 12 | p2p_id_seednode: str, 13 | port_proxy: int, 14 | port_first_observer: int, 15 | port_first_observer_rest_api: int, 16 | port_first_validator: int, 17 | port_first_validator_rest_api: int, 18 | ): 19 | self.host: str = host 20 | self.port_seednode: int = port_seednode 21 | self.port_seednode_rest_api: int = port_seednode_rest_api 22 | self.p2p_id_seednode: str = p2p_id_seednode 23 | self.port_proxy: int = port_proxy 24 | self.port_first_observer: int = port_first_observer 25 | self.port_first_observer_rest_api: int = port_first_observer_rest_api 26 | self.port_first_validator: int = port_first_validator 27 | self.port_first_validator_rest_api: int = port_first_validator_rest_api 28 | 29 | def get_name(self) -> str: 30 | return "networking" 31 | 32 | def _do_override(self, other: Dict[str, Any]): 33 | self.host = other.get("host", self.host) 34 | self.port_seednode = other.get("port_seednode", self.port_seednode) 35 | self.port_seednode_rest_api = other.get("port_seednode_rest_api", self.port_seednode_rest_api) 36 | self.p2p_id_seednode = other.get("p2p_id_seednode", self.p2p_id_seednode) 37 | self.port_proxy = other.get("port_proxy", self.port_proxy) 38 | self.port_first_observer = other.get("port_first_observer", self.port_first_observer) 39 | self.port_first_observer_rest_api = other.get("port_first_observer_rest_api", self.port_first_observer_rest_api) 40 | self.port_first_validator = other.get("port_first_validator", self.port_first_validator) 41 | self.port_first_validator_rest_api = other.get( 42 | "port_first_validator_rest_api", self.port_first_validator_rest_api 43 | ) 44 | 45 | def get_proxy_url(self) -> str: 46 | return f"http://{self.host}:{self.port_proxy}" 47 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/config_part.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Any, Dict 3 | 4 | from multiversx_sdk_cli.errors import UnknownConfigurationError 5 | 6 | logger = logging.getLogger("localnet") 7 | 8 | 9 | class ConfigPart: 10 | def __init__(self): 11 | pass 12 | 13 | def get_name(self) -> str: 14 | raise NotImplementedError() 15 | 16 | def override(self, other: Dict[str, Any]): 17 | self._validate_overriding_entries(other) 18 | self._do_override(other) 19 | 20 | def _validate_overriding_entries(self, overriding: Dict[str, Any]) -> None: 21 | allowed_entries = set(self.__dict__.keys()) 22 | overriding_entries = set(overriding.keys()) 23 | unknown_entries = overriding_entries - allowed_entries 24 | 25 | if unknown_entries: 26 | logger.error( 27 | f"""\ 28 | Unknown localnet configuration entries: {unknown_entries}. 29 | Please check the configuration of the localnet. 30 | For "{self.get_name()}", the allowed entries are: {allowed_entries}.""" 31 | ) 32 | raise UnknownConfigurationError(f"Unknown localnet configuration entries: {unknown_entries}") 33 | 34 | def _do_override(self, other: Dict[str, Any]) -> None: 35 | raise NotImplementedError() 36 | 37 | def to_dictionary(self) -> Dict[str, Any]: 38 | return self.__dict__ 39 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/config_root.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from pathlib import Path 4 | from typing import Any, Dict, List 5 | 6 | import toml 7 | 8 | from multiversx_sdk_cli.localnet import config_default 9 | from multiversx_sdk_cli.localnet.config_part import ConfigPart 10 | from multiversx_sdk_cli.localnet.config_sharding import Metashard, RegularShards 11 | from multiversx_sdk_cli.localnet.constants import METACHAIN_ID 12 | from multiversx_sdk_cli.localnet.node import Node 13 | 14 | logger = logging.getLogger("localnet") 15 | 16 | 17 | class ConfigRoot(ConfigPart): 18 | def __init__(self) -> None: 19 | self.general = config_default.general 20 | self.software = config_default.software 21 | self.metashard: Metashard = config_default.metashard 22 | self.shards: RegularShards = config_default.shards 23 | self.networking = config_default.networking 24 | 25 | def get_name(self) -> str: 26 | return "(configuration root)" 27 | 28 | def _do_override(self, other: Dict[str, Any]): 29 | self.general.override(other.get(self.general.get_name(), dict())) 30 | self.software.override(other.get(self.software.get_name(), dict())) 31 | self.metashard.override(other.get(self.metashard.get_name(), dict())) 32 | self.shards.override(other.get(self.shards.get_name(), dict())) 33 | self.networking.override(other.get(self.networking.get_name(), dict())) 34 | 35 | @classmethod 36 | def from_file(cls, path: Path): 37 | logger.info(f"Loading localnet configuration from: {path}") 38 | 39 | path = path.expanduser().resolve() 40 | instance = cls() 41 | local_config_dict = toml.load(str(path)) 42 | instance.override(local_config_dict) 43 | 44 | return instance 45 | 46 | def save(self, path: Path): 47 | path = path.expanduser().resolve() 48 | with open(path, "w") as f: 49 | toml.dump(self.to_dictionary(), f) 50 | 51 | logger.info(f"Saved localnet configuration to: {path}") 52 | 53 | def root(self) -> Path: 54 | return Path("localnet").expanduser().resolve() 55 | 56 | def seednode_folder(self): 57 | return self.root() / "seednode" 58 | 59 | def seednode_config_folder(self): 60 | return self.seednode_folder() / "config" 61 | 62 | def proxy_folder(self): 63 | return self.root() / "proxy" 64 | 65 | def proxy_config_folder(self): 66 | return self.proxy_folder() / "config" 67 | 68 | def all_nodes_folders(self): 69 | return self.validator_folders() + self.observer_folders() 70 | 71 | def all_nodes_config_folders(self): 72 | return self.validator_config_folders() + self.observer_config_folders() 73 | 74 | def genesis_time(self): 75 | return int(time.time()) + int(self.general.genesis_delay_seconds) 76 | 77 | def seednode_address(self): 78 | host = self.networking.host 79 | port = self.networking.port_seednode 80 | identifier = self.networking.p2p_id_seednode 81 | return f"/ip4/{host}/tcp/{port}/p2p/{identifier}" 82 | 83 | def seednode_api_interface(self): 84 | port = self.networking.port_seednode_rest_api 85 | return f"{self.networking.host}:{port}" 86 | 87 | def seednode_api_address(self): 88 | return f"http://{self.seednode_api_interface()}" 89 | 90 | def num_all_nodes(self) -> int: 91 | return self.num_all_validators() + self.num_all_observers() 92 | 93 | def num_all_validators(self) -> int: 94 | return self.shards.num_shards * self.shards.num_validators_per_shard + self.metashard.num_validators 95 | 96 | def num_all_observers(self) -> int: 97 | return self.shards.num_shards * self.shards.num_observers_per_shard + self.metashard.num_observers 98 | 99 | def all_nodes(self) -> List[Node]: 100 | return self.validators() + self.observers() 101 | 102 | def validators(self) -> List[Node]: 103 | first_port = self.networking.port_first_validator_rest_api 104 | nodes: List[Node] = [] 105 | 106 | for i, folder in enumerate(self.validator_folders()): 107 | shard = self._get_shard_of_validator(i) 108 | host = self.networking.host 109 | port = first_port + i 110 | nodes.append(Node(index=i, folder=folder, shard=str(shard), host=host, api_port=port)) 111 | 112 | return nodes 113 | 114 | def _get_shard_of_validator(self, observer_index: int) -> int: 115 | shard = int(observer_index // self.shards.num_validators_per_shard) 116 | return shard if shard < self.shards.num_shards else METACHAIN_ID 117 | 118 | def validator_folders(self): 119 | return [self.root() / "validator{:02}".format(i) for i in range(self.num_all_validators())] 120 | 121 | def validator_config_folders(self) -> List[Path]: 122 | return [folder / "config" for folder in self.validator_folders()] 123 | 124 | def observers(self) -> List[Node]: 125 | first_port = self.networking.port_first_observer_rest_api 126 | nodes: List[Node] = [] 127 | 128 | for i, folder in enumerate(self.observer_folders()): 129 | shard = self._get_shard_of_observer(i) 130 | host = self.networking.host 131 | port = first_port + i 132 | nodes.append(Node(index=i, folder=folder, shard=str(shard), host=host, api_port=port)) 133 | 134 | return nodes 135 | 136 | def _get_shard_of_observer(self, observer_index: int): 137 | shard = int(observer_index // self.shards.num_observers_per_shard) 138 | return shard if shard < self.shards.num_shards else METACHAIN_ID 139 | 140 | def observer_config_folders(self) -> List[Path]: 141 | return [folder / "config" for folder in self.observer_folders()] 142 | 143 | def observer_folders(self) -> List[Path]: 144 | return [self.root() / "observer{:02}".format(i) for i in range(self.num_all_observers())] 145 | 146 | def api_addresses_sharded_for_proxy_config(self) -> List[Dict[str, Any]]: 147 | nodes: List[Dict[str, Any]] = [] 148 | 149 | for node in self.observers(): 150 | nodes.append( 151 | { 152 | "ShardId": int(node.shard), 153 | "Address": node.api_address(), 154 | "Type": "Observer", 155 | } 156 | ) 157 | 158 | for node in self.validators(): 159 | nodes.append( 160 | { 161 | "ShardId": int(node.shard), 162 | "Address": node.api_address(), 163 | "Type": "Validator", 164 | } 165 | ) 166 | 167 | return nodes 168 | 169 | def to_dictionary(self) -> Dict[str, Any]: 170 | result: Dict[str, Any] = dict() 171 | result[self.general.get_name()] = self.general.to_dictionary() 172 | result[self.software.get_name()] = self.software.to_dictionary() 173 | result[self.metashard.get_name()] = self.metashard.to_dictionary() 174 | result[self.shards.get_name()] = self.shards.to_dictionary() 175 | result[self.networking.get_name()] = self.networking.to_dictionary() 176 | 177 | return result 178 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/config_sharding.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from multiversx_sdk_cli.localnet.config_part import ConfigPart 4 | 5 | 6 | class Metashard(ConfigPart): 7 | def __init__(self, consensus_size: int, num_observers: int, num_validators: int): 8 | self.consensus_size: int = consensus_size 9 | self.num_observers: int = num_observers 10 | self.num_validators: int = num_validators 11 | 12 | def get_name(self) -> str: 13 | return "metashard" 14 | 15 | def _do_override(self, other: Dict[str, Any]): 16 | self.consensus_size = other.get("consensus_size", self.consensus_size) 17 | self.num_observers = other.get("num_observers", self.num_observers) 18 | self.num_validators = other.get("num_validators", self.num_validators) 19 | 20 | 21 | class RegularShards(ConfigPart): 22 | def __init__( 23 | self, 24 | num_shards: int, 25 | consensus_size: int, 26 | num_observers_per_shard: int, 27 | num_validators_per_shard: int, 28 | ): 29 | self.num_shards: int = num_shards 30 | self.consensus_size: int = consensus_size 31 | self.num_observers_per_shard: int = num_observers_per_shard 32 | self.num_validators_per_shard: int = num_validators_per_shard 33 | 34 | def get_name(self) -> str: 35 | return "shards" 36 | 37 | def _do_override(self, other: Dict[str, Any]): 38 | self.num_shards = other.get("num_shards", self.num_shards) 39 | self.consensus_size = other.get("consensus_size", self.consensus_size) 40 | self.num_observers_per_shard = other.get("num_observers_per_shard", self.num_observers_per_shard) 41 | self.num_validators_per_shard = other.get("num_validators_per_shard", self.num_validators_per_shard) 42 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/config_software.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | from pathlib import Path 3 | from typing import Any, Dict 4 | 5 | from multiversx_sdk_cli.errors import KnownError 6 | from multiversx_sdk_cli.localnet.config_part import ConfigPart 7 | 8 | 9 | class SoftwareResolution(Enum): 10 | Remote = "remote" 11 | Local = "local" 12 | 13 | 14 | class Software(ConfigPart): 15 | def __init__(self, mx_chain_go: "SoftwareChainGo", mx_chain_proxy_go: "SoftwareChainProxyGo"): 16 | self.mx_chain_go = mx_chain_go 17 | self.mx_chain_proxy_go = mx_chain_proxy_go 18 | 19 | def get_name(self) -> str: 20 | return "software" 21 | 22 | def _do_override(self, other: Dict[str, Any]): 23 | self.mx_chain_go.override(other.get("mx_chain_go", {})) 24 | self.mx_chain_proxy_go.override(other.get("mx_chain_proxy_go", {})) 25 | 26 | def to_dictionary(self) -> Dict[str, Any]: 27 | return { 28 | "mx_chain_go": self.mx_chain_go.to_dictionary(), 29 | "mx_chain_proxy_go": self.mx_chain_proxy_go.to_dictionary(), 30 | } 31 | 32 | 33 | class SoftwareComponent(ConfigPart): 34 | def __init__( 35 | self, 36 | resolution: SoftwareResolution, 37 | archive_url: str, 38 | archive_download_folder: Path, 39 | archive_extraction_folder: Path, 40 | local_path: Path, 41 | ): 42 | self.resolution: SoftwareResolution = resolution 43 | self.archive_url: str = archive_url 44 | self.archive_download_folder: Path = archive_download_folder 45 | self.archive_extraction_folder: Path = archive_extraction_folder 46 | self.local_path: Path = local_path 47 | self._verify() 48 | 49 | def _do_override(self, other: Dict[str, Any]) -> None: 50 | self.resolution = SoftwareResolution(other.get("resolution", self.resolution)) 51 | self.archive_url = other.get("archive_url", self.archive_url) 52 | self.archive_download_folder = Path(other.get("archive_download_folder", self.archive_download_folder)) 53 | self.archive_extraction_folder = Path(other.get("archive_extraction_folder", self.archive_extraction_folder)) 54 | self.local_path = Path(other.get("local_path", self.local_path)) 55 | self._verify() 56 | 57 | def _verify(self): 58 | if self.resolution == SoftwareResolution.Remote: 59 | if not self.archive_url: 60 | raise KnownError( 61 | f"In configuration section '{self.get_name()}', resolution is '{self.resolution.value}', but 'archive_url' is bad (empty)" 62 | ) 63 | if self.resolution == SoftwareResolution.Local: 64 | if not self.get_local_path().is_dir(): 65 | raise KnownError( 66 | f"In configuration section '{self.get_name()}', resolution is '{self.resolution.value}', but 'local_path' is not a directory: {self.local_path}" 67 | ) 68 | 69 | def get_archive_download_folder(self) -> Path: 70 | return self.archive_download_folder.expanduser().resolve() 71 | 72 | def get_archive_extraction_folder(self) -> Path: 73 | return self.archive_extraction_folder.expanduser().resolve() 74 | 75 | def get_local_path(self): 76 | return self.local_path.expanduser().resolve() 77 | 78 | def get_path_within_source(self, relative_path: Path) -> Path: 79 | path = self._get_source_folder() / relative_path 80 | return path.expanduser().resolve() 81 | 82 | def _get_source_folder(self) -> Path: 83 | if self.resolution == SoftwareResolution.Remote: 84 | return self._locate_source_folder_in_archive_extraction_folder() 85 | if self.resolution == SoftwareResolution.Local: 86 | return self.local_path 87 | 88 | raise KnownError(f"Unknown resolution: {self.resolution}") 89 | 90 | def _locate_source_folder_in_archive_extraction_folder(self) -> Path: 91 | extraction_folder = self.get_archive_extraction_folder() 92 | 93 | # If has one subfolder, that one is the source code 94 | subfolders = list(extraction_folder.glob("*")) 95 | source_folder = subfolders[0] if len(subfolders) == 1 else extraction_folder 96 | # Heuristic to check if this is a valid source code folder 97 | assert (source_folder / "go.mod").exists(), f"This is not a valid source code folder: {source_folder}" 98 | return source_folder 99 | 100 | def to_dictionary(self) -> Dict[str, Any]: 101 | return { 102 | "resolution": self.resolution.value, 103 | "archive_url": self.archive_url, 104 | "archive_download_folder": str(self.archive_download_folder), 105 | "archive_extraction_folder": str(self.archive_extraction_folder), 106 | "local_path": str(self.local_path) if self.local_path else None, 107 | } 108 | 109 | 110 | class SoftwareChainGo(SoftwareComponent): 111 | def get_name(self) -> str: 112 | return "mx_chain_go" 113 | 114 | def get_cmd_node_folder(self): 115 | folder = self._get_cmd_folder() / "node" 116 | folder_must_exist(folder) 117 | return folder 118 | 119 | def get_cmd_seednode_folder(self): 120 | folder = self._get_cmd_folder() / "seednode" 121 | folder_must_exist(folder) 122 | return folder 123 | 124 | def _get_cmd_folder(self): 125 | folder = self.get_path_within_source(Path("cmd")) 126 | folder_must_exist(folder) 127 | return folder 128 | 129 | def is_node_built(self): 130 | return (self.get_cmd_node_folder() / "node").exists() 131 | 132 | def is_seednode_built(self): 133 | return (self.get_cmd_seednode_folder() / "seednode").exists() 134 | 135 | def get_node_config_folder(self): 136 | return self.get_cmd_node_folder() / "config" 137 | 138 | def get_seednode_config_folder(self): 139 | return self.get_cmd_seednode_folder() / "config" 140 | 141 | def node_config_must_exist(self): 142 | folder_must_exist(self.get_node_config_folder()) 143 | 144 | def seednode_config_must_exist(self): 145 | folder_must_exist(self.get_seednode_config_folder()) 146 | 147 | 148 | class SoftwareChainProxyGo(SoftwareComponent): 149 | def get_name(self) -> str: 150 | return "mx_chain_proxy_go" 151 | 152 | def get_cmd_proxy_folder(self): 153 | folder = self._get_cmd_folder() / "proxy" 154 | folder_must_exist(folder) 155 | return folder 156 | 157 | def _get_cmd_folder(self): 158 | folder = self.get_path_within_source(Path("cmd")) 159 | folder_must_exist(folder) 160 | return folder 161 | 162 | def is_proxy_built(self): 163 | return (self.get_cmd_proxy_folder() / "proxy").exists() 164 | 165 | def get_proxy_config_folder(self): 166 | return self.get_cmd_proxy_folder() / "config" 167 | 168 | def proxy_config_must_exist(self): 169 | folder_must_exist(self.get_proxy_config_folder()) 170 | 171 | 172 | def folder_must_exist(path: Path) -> None: 173 | if not path.exists(): 174 | raise KnownError(f"Folder does not exist: {path}") 175 | 176 | 177 | def file_must_exist(path: Path) -> None: 178 | if not path.exists(): 179 | raise KnownError(f"File does not exist: {path}") 180 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/constants.py: -------------------------------------------------------------------------------- 1 | import stat 2 | 3 | METACHAIN_ID = 4294967295 4 | NETWORK_MONITORING_INTERVAL_IN_SECONDS = 1 5 | # Read, write and execute by owner, read and execute by group and others 6 | FILE_MODE_EXECUTABLE = ( 7 | stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH 8 | ) 9 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/genesis.py: -------------------------------------------------------------------------------- 1 | from multiversx_sdk_cli.localnet import wallets 2 | 3 | 4 | def get_owner_of_genesis_contracts(): 5 | users = wallets.get_users() 6 | return users["alice"] 7 | 8 | 9 | def is_last_user(nickname: str) -> bool: 10 | return nickname == "mike" 11 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/genesis_json.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | from multiversx_sdk import Account 4 | 5 | from multiversx_sdk_cli.localnet import wallets 6 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 7 | from multiversx_sdk_cli.localnet.genesis import is_last_user 8 | 9 | ENTIRE_SUPPLY = 20000000000000000000000000 10 | # For localnet, we delegate for 1 node 11 | DELEGATED_VALUE = 2500000000000000000000 12 | 13 | 14 | def build(config: ConfigRoot) -> List[Any]: 15 | num_validators = config.num_all_validators() 16 | genesis_items: List[Dict[str, Any]] = [] 17 | remaining_supply = ENTIRE_SUPPLY 18 | 19 | for nickname, account in wallets.get_validator_wallets(num_validators).items(): 20 | value = 2500000000000000000000 21 | entry = _build_validator_entry(nickname, account, value) 22 | genesis_items.append(entry) 23 | remaining_supply -= value 24 | 25 | for nickname, account in wallets.get_users().items(): 26 | # The last user (mike) gets all remaining tokens 27 | value = remaining_supply if is_last_user(nickname) else 100000000000000000000000 28 | entry = _build_user_entry(nickname, account, value) 29 | genesis_items.append(entry) 30 | remaining_supply -= value 31 | 32 | return genesis_items 33 | 34 | 35 | def _build_validator_entry(nickname: str, account: Account, value: int) -> Dict[str, Any]: 36 | return { 37 | "nickname": nickname, 38 | "address": account.address.to_bech32(), 39 | "supply": str(value), 40 | "balance": "0", 41 | "stakingvalue": str(value), 42 | "delegation": {"address": "", "value": "0"}, 43 | } 44 | 45 | 46 | def _build_user_entry(nickname: str, account: Account, value: int) -> Dict[str, Any]: 47 | return { 48 | "nickname": nickname, 49 | "address": account.address.to_bech32(), 50 | "supply": str(value), 51 | "balance": str(value), 52 | "stakingvalue": "0", 53 | "delegation": {"address": "", "value": "0"}, 54 | } 55 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/genesis_smart_contracts_json.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from multiversx_sdk_cli.localnet import genesis 4 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 5 | 6 | 7 | def patch(data: Any, config: ConfigRoot): 8 | owner = genesis.get_owner_of_genesis_contracts() 9 | 10 | delegation_config = data[0] 11 | dns_config = data[1] 12 | 13 | delegation_config["owner"] = owner.address.bech32() 14 | dns_config["owner"] = owner.address.bech32() 15 | # registration price = 100 atto-EGLD 16 | dns_config["init-parameters"] = "0064" 17 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/libraries.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import shutil 4 | from pathlib import Path 5 | from typing import List 6 | 7 | from multiversx_sdk_cli.localnet.constants import FILE_MODE_EXECUTABLE 8 | 9 | logger = logging.getLogger("localnet") 10 | 11 | 12 | def copy_libraries(source: Path, destination: Path): 13 | libraries: List[Path] = list(source.glob("*.dylib")) + list(source.glob("*.so")) 14 | 15 | for library in libraries: 16 | logger.debug(f"Copying {library} to {destination}") 17 | shutil.copy(library, destination) 18 | os.chmod(destination / library.name, FILE_MODE_EXECUTABLE) 19 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/node.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | class Node: 5 | def __init__(self, index: int, folder: Path, shard: str, host: str, api_port: int) -> None: 6 | self.index = index 7 | self.folder = folder 8 | self.shard = shard 9 | self.host = host 10 | self.api_port = api_port 11 | 12 | def key_file_path(self): 13 | return self.folder / "config" / "validatorKey.pem" 14 | 15 | def api_address(self): 16 | return f"http://{self.api_interface()}" 17 | 18 | def api_interface(self): 19 | return f"{self.host}:{self.api_port}" 20 | 21 | def __repr__(self) -> str: 22 | return f"Node {self.index}, shard={self.shard}, port={self.api_port}, folder={self.folder}" 23 | 24 | 25 | class NodeStatus: 26 | def __init__(self, nonce: int) -> None: 27 | self.nonce = nonce 28 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/node_config_toml.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict 2 | 3 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 4 | from multiversx_sdk_cli.localnet.nodes_setup_json import CHAIN_ID 5 | 6 | ConfigDict = Dict[str, Any] 7 | 8 | 9 | def patch_config(data: ConfigDict, config: ConfigRoot): 10 | data["GeneralSettings"]["ChainID"] = CHAIN_ID 11 | 12 | # "--operation-mode=historical-balances" is not available for nodes, 13 | # since << validator cannot be a full archive node >>, 14 | # but we attempt to set the "deep-history" mode as follows: 15 | data["DbLookupExtensions"]["Enabled"] = True 16 | data["GeneralSettings"]["StartInEpochEnabled"] = False 17 | data["StateTriesConfig"]["AccountsStatePruningEnabled"] = False 18 | data["StoragePruning"]["ObserverCleanOldEpochsData"] = False 19 | data["StoragePruning"]["AccountsTrieCleanOldEpochsData"] = False 20 | 21 | # Make epochs shorter 22 | epoch_start_config: ConfigDict = dict() 23 | epoch_start_config["RoundsPerEpoch"] = config.general.rounds_per_epoch 24 | epoch_start_config["MinRoundsBetweenEpochs"] = int(config.general.rounds_per_epoch / 4) 25 | 26 | data["EpochStartConfig"].update(epoch_start_config) 27 | data["WebServerAntiflood"]["VmQueryDelayAfterStartInSec"] = 30 28 | 29 | # Always use the latest VM 30 | data["VirtualMachine"]["Execution"]["WasmVMVersions"] = [{"StartEpoch": 0, "Version": "*"}] 31 | data["VirtualMachine"]["Querying"]["WasmVMVersions"] = [{"StartEpoch": 0, "Version": "*"}] 32 | 33 | # Adjust "ChainParametersByEpoch" (for Andromeda) 34 | chain_parameters_by_epoch = data["GeneralSettings"].get("ChainParametersByEpoch", []) 35 | 36 | if chain_parameters_by_epoch: 37 | # For convenience, keep a single entry ... 38 | chain_parameters_by_epoch.clear() 39 | chain_parameters_by_epoch.append({}) 40 | 41 | # ... and set the activation epoch to 0 42 | chain_parameters_by_epoch[0]["EnableEpoch"] = 0 43 | chain_parameters_by_epoch[0]["RoundDuration"] = config.general.round_duration_milliseconds 44 | chain_parameters_by_epoch[0]["ShardConsensusGroupSize"] = config.shards.consensus_size 45 | chain_parameters_by_epoch[0]["ShardMinNumNodes"] = config.shards.num_validators_per_shard 46 | chain_parameters_by_epoch[0]["MetachainConsensusGroupSize"] = config.metashard.consensus_size 47 | chain_parameters_by_epoch[0]["MetachainMinNumNodes"] = config.metashard.num_validators 48 | 49 | 50 | def patch_api(data: ConfigDict, config: ConfigRoot): 51 | routes = data["APIPackages"]["transaction"]["Routes"] 52 | for route in routes: 53 | route["Open"] = True 54 | 55 | 56 | def patch_enable_epochs(data: ConfigDict, config: ConfigRoot): 57 | enable_epochs = data["EnableEpochs"] 58 | enable_epochs["SCDeployEnableEpoch"] = 0 59 | enable_epochs["BuiltInFunctionsEnableEpoch"] = 0 60 | enable_epochs["RelayedTransactionsEnableEpoch"] = 0 61 | enable_epochs["PenalizedTooMuchGasEnableEpoch"] = 0 62 | enable_epochs["AheadOfTimeGasUsageEnableEpoch"] = 0 63 | enable_epochs["GasPriceModifierEnableEpoch"] = 0 64 | enable_epochs["RepairCallbackEnableEpoch"] = 0 65 | enable_epochs["ReturnDataToLastTransferEnableEpoch"] = 0 66 | enable_epochs["SenderInOutTransferEnableEpoch"] = 0 67 | enable_epochs["ESDTEnableEpoch"] = 0 68 | enable_epochs["IncrementSCRNonceInMultiTransferEnableEpoch"] = 0 69 | enable_epochs["ESDTMultiTransferEnableEpoch"] = 0 70 | enable_epochs["GlobalMintBurnDisableEpoch"] = 0 71 | enable_epochs["ESDTTransferRoleEnableEpoch"] = 0 72 | enable_epochs["BuiltInFunctionOnMetaEnableEpoch"] = 0 73 | enable_epochs["MultiESDTTransferFixOnCallBackOnEnableEpoch"] = 0 74 | enable_epochs["ESDTNFTCreateOnMultiShard"] = 0 75 | enable_epochs["MetaESDTSetEnableEpoch"] = 0 76 | enable_epochs["DelegationManagerEnableEpoch"] = 0 77 | 78 | max_nodes_change_enable_epoch = enable_epochs["MaxNodesChangeEnableEpoch"] 79 | last_entry = max_nodes_change_enable_epoch[-1] 80 | penultimate_entry = max_nodes_change_enable_epoch[-2] 81 | last_entry["MaxNumNodes"] = ( 82 | penultimate_entry["MaxNumNodes"] - (config.shards.num_shards + 1) * penultimate_entry["NodesToShufflePerShard"] 83 | ) 84 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/nodes_setup_json.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, List 2 | 3 | from multiversx_sdk_cli.localnet import wallets 4 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 5 | 6 | CHAIN_ID = "localnet" 7 | 8 | 9 | def build(config: ConfigRoot) -> Any: 10 | num_validators = config.num_all_validators() 11 | initial_nodes: List[Dict[str, str]] = [] 12 | 13 | for nickname, [pubkey, account] in wallets.get_validators(num_validators).items(): 14 | entry = { 15 | "nickname": nickname, 16 | "address": account.address.to_bech32(), 17 | "pubkey": pubkey, 18 | } 19 | 20 | initial_nodes.append(entry) 21 | 22 | # Then, patch the list of initial nodes, so that higher indexes will become metachain nodes. 23 | num_metachain_nodes = config.metashard.num_validators 24 | num_nodes = len(initial_nodes) 25 | initial_nodes = initial_nodes[num_nodes - num_metachain_nodes :] + initial_nodes[: num_nodes - num_metachain_nodes] 26 | 27 | return { 28 | "startTime": config.genesis_time(), 29 | "roundDuration": config.general.round_duration_milliseconds, 30 | "consensusGroupSize": config.shards.consensus_size, 31 | "minNodesPerShard": config.shards.consensus_size, 32 | "metaChainConsensusGroupSize": config.metashard.consensus_size, 33 | "metaChainMinNodes": config.metashard.num_validators, 34 | "hysteresis": 0, 35 | "adaptivity": False, 36 | "chainID": CHAIN_ID, 37 | "minTransactionVersion": 1, 38 | "initialNodes": initial_nodes, 39 | } 40 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/p2p_toml.py: -------------------------------------------------------------------------------- 1 | from typing import Any 2 | 3 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 4 | 5 | PROTOCOL_ID = "/erd/kad/sandbox" 6 | 7 | 8 | def patch(data: Any, config: ConfigRoot, node_index: int, port_first: int) -> Any: 9 | data["Node"]["Port"] = str(port_first + node_index) 10 | data["Node"]["ThresholdMinConnectedPeers"] = 1 11 | data["KadDhtPeerDiscovery"]["InitialPeerList"] = [config.seednode_address()] 12 | data["KadDhtPeerDiscovery"]["ProtocolID"] = PROTOCOL_ID 13 | data["Sharding"]["Type"] = "NilListSharder" 14 | 15 | 16 | def patch_for_seednode(data: Any, config: ConfigRoot): 17 | port_seednode = config.networking.port_seednode 18 | 19 | data["Node"]["Port"] = str(port_seednode) 20 | data["Node"]["MaximumExpectedPeerCount"] = 16 21 | data["KadDhtPeerDiscovery"]["ProtocolID"] = PROTOCOL_ID 22 | data["Sharding"]["Type"] = "NilListSharder" 23 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/seednode_p2pKey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for 16Uiu2HAkx4QqgXXDdHdUWbLu5kxhd3Uo2hqB2FfCxmxH5Sd7bZFk----- 2 | NTM2Mjk0Zjk2Zjk5YjdlOTUzNWVkOTRmNDAzOGEyOTY2ZGY0ZDkwOTQ4MDU5YTNi 3 | MDM4MjI4NzNjZjBkYjRlMw== 4 | -----END PRIVATE KEY for 16Uiu2HAkx4QqgXXDdHdUWbLu5kxhd3Uo2hqB2FfCxmxH5Sd7bZFk----- 5 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/step_build_software.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | from pathlib import Path 4 | from typing import Dict, List 5 | 6 | from multiversx_sdk_cli import dependencies, utils, workstation 7 | from multiversx_sdk_cli.errors import KnownError 8 | from multiversx_sdk_cli.localnet import libraries 9 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 10 | 11 | logger = logging.getLogger("localnet") 12 | 13 | 14 | def build(configfile: Path, software_components: List[str]): 15 | config = ConfigRoot.from_file(configfile) 16 | 17 | golang = dependencies.get_golang() 18 | golang_env = golang.get_env() 19 | 20 | if "node" in software_components: 21 | logger.info("Building node...") 22 | 23 | cmd_node = config.software.mx_chain_go.get_cmd_node_folder() 24 | _do_build(cmd_node, golang_env) 25 | _copy_wasmer_libs(config, cmd_node) 26 | _set_rpath(cmd_node / "node") 27 | 28 | if "seednode" in software_components: 29 | logger.info("Building seednode...") 30 | 31 | cmd_seednode = config.software.mx_chain_go.get_cmd_seednode_folder() 32 | _do_build(cmd_seednode, golang_env) 33 | _copy_wasmer_libs(config, cmd_seednode) 34 | _set_rpath(cmd_seednode / "seednode") 35 | 36 | if "proxy" in software_components: 37 | logger.info("Building proxy...") 38 | 39 | cmd_proxy = config.software.mx_chain_proxy_go.get_cmd_proxy_folder() 40 | _do_build(cmd_proxy, golang_env) 41 | 42 | 43 | def _do_build(cwd: Path, env: Dict[str, str]): 44 | return_code = subprocess.check_call(["go", "build"], cwd=cwd, env=env) 45 | if return_code != 0: 46 | raise KnownError(f"error code = {return_code}, see output") 47 | 48 | 49 | def _copy_wasmer_libs(config: ConfigRoot, destination: Path): 50 | golang = dependencies.get_golang() 51 | vm_go_folder_name = _get_chain_vm_go_folder_name(config) 52 | vm_go_path = golang.get_gopath() / "pkg" / "mod" / vm_go_folder_name 53 | wasmer_path = vm_go_path / "wasmer" 54 | wasmer2_path = vm_go_path / "wasmer2" 55 | 56 | libraries.copy_libraries(wasmer_path, destination) 57 | libraries.copy_libraries(wasmer2_path, destination) 58 | 59 | 60 | def _get_chain_vm_go_folder_name(config: ConfigRoot) -> str: 61 | go_mod = config.software.mx_chain_go.get_path_within_source(Path("go.mod")) 62 | lines = utils.read_lines(go_mod) 63 | line = [line for line in lines if "github.com/multiversx/mx-chain-vm-go" in line][0] 64 | parts = line.split() 65 | return f"{parts[0]}@{parts[1]}" 66 | 67 | 68 | def _set_rpath(cmd_path: Path): 69 | """ 70 | Set the rpath of the executable to the current directory, on a best-effort basis. 71 | 72 | For other occurrences of this approach, see: 73 | - https://github.com/multiversx/mx-chain-scenario-cli-go/blob/master/.github/workflows/on_release_attach_artifacts.yml 74 | """ 75 | 76 | if not workstation.is_osx(): 77 | # We're only patching the executable on macOS. 78 | # For Linux, we're leveraging LD_LIBRARY_PATH to resolve the libraries. 79 | return 80 | 81 | try: 82 | subprocess.check_call(["install_name_tool", "-add_rpath", "@loader_path", cmd_path]) 83 | except Exception as e: 84 | # In most cases, this isn't critical (libraries might be found among the downloaded Go packages). 85 | logger.warning(f"Failed to set rpath of {cmd_path}: {e}") 86 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/step_clean.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from multiversx_sdk_cli import utils 5 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 6 | 7 | logger = logging.getLogger("localnet") 8 | 9 | 10 | def clean(configfile: Path): 11 | logger.info("clean()") 12 | 13 | config = ConfigRoot.from_file(configfile) 14 | utils.remove_folder(config.root()) 15 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/step_new.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 5 | 6 | logger = logging.getLogger("localnet") 7 | 8 | 9 | def new_config(configfile: Path): 10 | logger.info("new_config()") 11 | 12 | configfile = configfile.expanduser().resolve() 13 | 14 | if configfile.exists(): 15 | logger.info(f"Configuration file already exists: {configfile}") 16 | return 17 | 18 | config = ConfigRoot() 19 | config.save(configfile) 20 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/step_prerequisites.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import shutil 3 | import urllib.request 4 | from pathlib import Path 5 | 6 | from multiversx_sdk_cli import dependencies 7 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 8 | from multiversx_sdk_cli.localnet.config_software import ( 9 | SoftwareComponent, 10 | SoftwareResolution, 11 | ) 12 | 13 | logger = logging.getLogger("localnet") 14 | 15 | 16 | def fetch_prerequisites(configfile: Path): 17 | logger.info("fetch_prerequisites()") 18 | 19 | config = ConfigRoot.from_file(configfile) 20 | 21 | dependencies.install_module("testwallets", overwrite=True) 22 | 23 | if config.software.mx_chain_go.resolution == SoftwareResolution.Remote: 24 | download_software_component(config.software.mx_chain_go) 25 | 26 | if config.software.mx_chain_proxy_go.resolution == SoftwareResolution.Remote: 27 | download_software_component(config.software.mx_chain_proxy_go) 28 | 29 | config.software.mx_chain_go.node_config_must_exist() 30 | config.software.mx_chain_go.seednode_config_must_exist() 31 | config.software.mx_chain_proxy_go.proxy_config_must_exist() 32 | 33 | is_node_built = config.software.mx_chain_go.is_node_built() 34 | is_seednode_built = config.software.mx_chain_go.is_seednode_built() 35 | is_proxy_built = config.software.mx_chain_proxy_go.is_proxy_built() 36 | 37 | is_golang_needed = not (is_node_built and is_seednode_built and is_proxy_built) 38 | if is_golang_needed: 39 | dependencies.install_module("golang") 40 | 41 | 42 | def download_software_component(component: SoftwareComponent): 43 | download_folder = component.get_archive_download_folder() 44 | extraction_folder = component.get_archive_extraction_folder() 45 | url = component.archive_url 46 | 47 | shutil.rmtree(str(download_folder), ignore_errors=True) 48 | shutil.rmtree(str(extraction_folder), ignore_errors=True) 49 | 50 | download_folder.mkdir(parents=True, exist_ok=True) 51 | extraction_folder.mkdir(parents=True, exist_ok=True) 52 | archive_extension = url.split(".")[-1] 53 | download_path = download_folder / f"archive.{archive_extension}" 54 | 55 | logger.info(f"Downloading archive {url} to {download_path}") 56 | urllib.request.urlretrieve(url, download_path) 57 | 58 | logger.info(f"Unpacking archive {download_path} to {extraction_folder}") 59 | shutil.unpack_archive(download_path, extraction_folder, format="zip") 60 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/localnet/wallets.py: -------------------------------------------------------------------------------- 1 | import shutil 2 | from collections import OrderedDict 3 | from pathlib import Path 4 | from typing import Dict, Tuple 5 | 6 | from multiversx_sdk import Account, ValidatorPEM 7 | 8 | from multiversx_sdk_cli import errors, utils 9 | from multiversx_sdk_cli.workstation import get_tools_folder 10 | 11 | MAX_NUM_NODES = 12 12 | 13 | 14 | def copy_all_to(destination: str): 15 | shutil.copytree(_get_folder(), destination) 16 | 17 | 18 | def copy_validator_key_to(validator_index: int, destination: str): 19 | shutil.copy(get_validator_key_file(validator_index), destination) 20 | 21 | 22 | def get_validator_key_file(validator_index: int): 23 | _guard_validator_index(validator_index) 24 | return _get_validators_folder().joinpath("validatorKey{:02}.pem".format(validator_index)) 25 | 26 | 27 | def get_observer_key_file(observer_index: int): 28 | _guard_validator_index(observer_index) 29 | return _get_observers_folder().joinpath("observerKey{:02}.pem".format(observer_index)) 30 | 31 | 32 | def get_validator_wallets(num_validators: int) -> Dict[str, Account]: 33 | result: Dict[str, Account] = {} 34 | 35 | for i in range(0, num_validators): 36 | pem_file = get_validator_wallet_file(i) 37 | nickname = "validator{:02}".format(i) 38 | account = Account.new_from_pem(file_path=pem_file) 39 | result[nickname] = account 40 | 41 | return result 42 | 43 | 44 | def get_validators(num_validators: int) -> Dict[str, Tuple[str, Account]]: 45 | result: Dict[str, Tuple[str, Account]] = {} 46 | 47 | for i in range(0, num_validators): 48 | validator_pem_file = get_validator_key_file(i) 49 | pem_from_file = ValidatorPEM.from_file(validator_pem_file) 50 | pubkey = pem_from_file.label 51 | 52 | pem_file = get_validator_wallet_file(i) 53 | nickname = "validator{:02}".format(i) 54 | account = Account.new_from_pem(file_path=pem_file) 55 | result[nickname] = (pubkey, account) 56 | 57 | return result 58 | 59 | 60 | def get_validator_wallet_file(validator_index: int): 61 | _guard_validator_index(validator_index) 62 | return _get_validators_folder().joinpath("wallet{:02}.pem".format(validator_index)) 63 | 64 | 65 | def _guard_validator_index(validator_index: int): 66 | if validator_index >= MAX_NUM_NODES: 67 | raise errors.KnownError(f"Invalid validator index: {validator_index} >= {MAX_NUM_NODES}.") 68 | 69 | 70 | def _get_validators_folder(): 71 | return _get_folder().joinpath("validators") 72 | 73 | 74 | def _get_observers_folder(): 75 | return _get_folder().joinpath("observers") 76 | 77 | 78 | def get_users() -> Dict[str, Account]: 79 | result: Dict[str, Account] = OrderedDict() 80 | 81 | for pem_file in sorted(utils.list_files(_get_users_folder(), ".pem")): 82 | nickname = Path(pem_file).stem 83 | account = Account.new_from_pem(file_path=pem_file) 84 | result[nickname] = account 85 | 86 | return result 87 | 88 | 89 | def _get_users_folder(): 90 | return _get_folder().joinpath("users") 91 | 92 | 93 | def _get_folder(): 94 | return get_tools_folder() / "testwallets" / "latest" 95 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/myprocess.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import subprocess 3 | from pathlib import Path 4 | from typing import Any, Optional, Union 5 | 6 | from multiversx_sdk_cli import errors 7 | 8 | logger = logging.getLogger("myprocess") 9 | 10 | 11 | def run_process( 12 | args: list[str], 13 | env: Any = None, 14 | dump_to_stdout: bool = True, 15 | cwd: Optional[Union[str, Path]] = None, 16 | ) -> str: 17 | logger.info(f"run_process: {args}, in folder: {cwd}") 18 | 19 | try: 20 | output = subprocess.check_output( 21 | args, 22 | shell=False, 23 | universal_newlines=True, 24 | stderr=subprocess.STDOUT, 25 | env=env, 26 | cwd=cwd, 27 | ) 28 | logger.info("Successful run. Output:") 29 | if dump_to_stdout: 30 | print(output or "[No output]") 31 | return output 32 | except subprocess.CalledProcessError as error: 33 | raise errors.ExternalProcessError(error.cmd, error.output) 34 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/sign_verify.py: -------------------------------------------------------------------------------- 1 | from typing import Protocol 2 | 3 | from multiversx_sdk import ( 4 | Address, 5 | Message, 6 | MessageComputer, 7 | UserVerifier, 8 | ValidatorPublicKey, 9 | ValidatorSigner, 10 | ) 11 | 12 | 13 | # fmt: off 14 | class IAccount(Protocol): 15 | address: Address 16 | 17 | def sign_message(self, message: Message) -> bytes: 18 | ... 19 | # fmt: off 20 | 21 | 22 | class SignedMessage: 23 | def __init__(self, address: str, message: str, signature: str) -> None: 24 | self.address = address 25 | self.message = message 26 | 27 | hex_prefixes = ["0x", "0X"] 28 | signature_start_sequence = signature[0:2] 29 | 30 | if signature_start_sequence in hex_prefixes: 31 | signature = signature[2:] 32 | 33 | self.signature = signature 34 | 35 | def verify_user_signature(self) -> bool: 36 | verifiable_message = Message(self.message.encode()) 37 | verifiable_message.signature = bytes.fromhex(self.signature) 38 | message_computer = MessageComputer() 39 | 40 | verifier = UserVerifier.from_address(Address.new_from_bech32(self.address)) 41 | is_signed = verifier.verify( 42 | message_computer.compute_bytes_for_signing(verifiable_message), 43 | verifiable_message.signature, 44 | ) 45 | return is_signed 46 | 47 | def verify_validator_signature(self) -> bool: 48 | validator_pubkey = ValidatorPublicKey(bytes.fromhex(self.address)) 49 | return validator_pubkey.verify( 50 | self.message.encode(), 51 | bytes.fromhex(self.signature), 52 | ) 53 | 54 | def to_dictionary(self) -> dict[str, str]: 55 | return { 56 | "address": self.address, 57 | "message": self.message, 58 | "signature": "0x" + self.signature, 59 | } 60 | 61 | 62 | def sign_message(message: str, account: IAccount) -> SignedMessage: 63 | signature = account.sign_message(Message(message.encode())) 64 | return SignedMessage(account.address.to_bech32(), message, signature.hex()) 65 | 66 | 67 | def sign_message_by_validator(message: str, validator: ValidatorSigner) -> SignedMessage: 68 | signature = validator.sign(message.encode()) 69 | return SignedMessage(validator.get_pubkey().hex(), message, signature.hex()) 70 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/simulation.py: -------------------------------------------------------------------------------- 1 | from collections import OrderedDict 2 | from typing import Any, Protocol 3 | 4 | from multiversx_sdk import Transaction, TransactionOnNetwork 5 | 6 | from multiversx_sdk_cli.utils import ISerializable 7 | 8 | 9 | # fmt: off 10 | class INetworkProvider(Protocol): 11 | def simulate_transaction(self, transaction: Transaction) -> TransactionOnNetwork: 12 | ... 13 | # fmt: on 14 | 15 | 16 | class Simulation(ISerializable): 17 | def __init__(self, simulate_response: TransactionOnNetwork) -> None: 18 | self.simulation_response = simulate_response 19 | 20 | def to_dictionary(self) -> dict[str, Any]: 21 | dictionary: dict[str, Any] = OrderedDict() 22 | dictionary["execution"] = self.simulation_response.raw 23 | 24 | return dictionary 25 | 26 | 27 | class Simulator: 28 | def __init__(self, proxy: INetworkProvider) -> None: 29 | self.proxy = proxy 30 | 31 | def run(self, transaction: Transaction) -> Simulation: 32 | simulation_response = self.proxy.simulate_transaction(transaction) 33 | 34 | return Simulation(simulation_response) 35 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-py-cli/e2da34bd5cb490ad4d99af4dfcffa54e44c1d4af/multiversx_sdk_cli/tests/__init__.py -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | # function executed right after test items collected but before test run 5 | def pytest_collection_modifyitems(config, items): 6 | if not config.getoption("-m"): 7 | skip_me = pytest.mark.skip(reason="require_localnet will only run if explicitly set to with -m") 8 | for item in items: 9 | if "require_localnet" in item.keywords: 10 | item.add_marker(skip_me) 11 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/local_verify_server.py: -------------------------------------------------------------------------------- 1 | import json 2 | from http.server import BaseHTTPRequestHandler, HTTPServer 3 | 4 | HOST = "localhost" 5 | PORT = 7777 6 | 7 | 8 | class HTTP(BaseHTTPRequestHandler): 9 | def do_POST(self): 10 | self.send_response(200) 11 | self.send_header("Content-type", "application/json") 12 | self.end_headers() 13 | 14 | if self.path == "/initialise": 15 | response = {"token": 7890} 16 | self.wfile.write(bytes(json.dumps(response), "utf-8")) 17 | 18 | if self.path == "/verify": 19 | response = {"status": "sent to verification"} 20 | self.wfile.write(bytes(json.dumps(response), "utf-8")) 21 | 22 | 23 | server = HTTPServer((HOST, PORT), HTTP) 24 | print("Server running...") 25 | 26 | server.serve_forever() 27 | server.server_close() 28 | 29 | print("Server closed!") 30 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/shared.sh: -------------------------------------------------------------------------------- 1 | # compatibility with MacOS ( https://stackoverflow.com/a/9484017 ) 2 | function absolute_path() { 3 | DIR="${1%/*}" 4 | (cd "$DIR" && echo "$(pwd -P)") 5 | } 6 | 7 | export PYTHONPATH=$(absolute_path ../../) 8 | echo "PYTHONPATH = ${PYTHONPATH}" 9 | 10 | CLI="python3 -m multiversx_sdk_cli.cli" 11 | SANDBOX=./testdata-out/SANDBOX 12 | USERS=~/multiversx-sdk/testwallets/latest/users 13 | VALIDATORS=~/multiversx-sdk/testwallets/latest/validators 14 | DENOMINATION="000000000000000000" 15 | PROXY="${PROXY:-http://localhost:7950}" 16 | CHAIN_ID="${CHAIN_ID:-localnet}" 17 | TestUser=./testdata/testUser.pem 18 | TestUser2=./testdata/testUser2.pem 19 | RUST_VERSION="stable" 20 | 21 | cleanSandbox() { 22 | rm -rf ${SANDBOX} 23 | mkdir -p ${SANDBOX} 24 | } 25 | 26 | assertFileExists() { 27 | if [ ! -f "$1" ] 28 | then 29 | echo "Error: file [$1] does not exist!" 1>&2 30 | return 1 31 | fi 32 | 33 | return 0 34 | } 35 | 36 | assertFileDoesNotExist() { 37 | if [ -f "$1" ] 38 | then 39 | echo "Error: expected file [$1] to be missing, but it exists!" 1>&2 40 | return 1 41 | fi 42 | 43 | return 0 44 | } 45 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_accounts.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import pytest 4 | from multiversx_sdk import Account 5 | 6 | 7 | def test_load_account_from_keystore_without_kind(): 8 | alice_json = Path(__file__).parent / "testdata" / "alice.json" 9 | account = Account.new_from_keystore(file_path=alice_json, password="password") 10 | assert account.address.to_bech32() == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" 11 | 12 | with pytest.raises(Exception): 13 | _ = Account.new_from_keystore(file_path=alice_json, password="wrong_password") 14 | 15 | 16 | def test_load_account_from_keystore_with_kind_secret_key(): 17 | keystore_path = Path(__file__).parent / "testdata" / "aliceWithKindSecretKey.json" 18 | account = Account.new_from_keystore(file_path=keystore_path, password="password") 19 | assert account.address.to_bech32() == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" 20 | 21 | with pytest.raises(Exception): 22 | _ = Account.new_from_keystore(file_path=keystore_path, password="wrong_password") 23 | 24 | 25 | def test_load_account_from_keystore_with_kind_mnemonic(): 26 | keystore_path = Path(__file__).parent / "testdata" / "withDummyMnemonic.json" 27 | account = Account.new_from_keystore(file_path=keystore_path, password="password") 28 | assert account.address.to_bech32() == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" 29 | 30 | with pytest.raises(Exception): 31 | _ = Account.new_from_keystore(file_path=keystore_path, password="wrong_password") 32 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_cli_contracts.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "./shared.sh" 4 | 5 | testTrivialCommands() { 6 | echo "testTrivialCommands" 7 | ${CLI} contract templates 8 | } 9 | 10 | testCreateContracts() { 11 | echo "testCreateContracts" 12 | ${CLI} contract new --template adder --path ${SANDBOX} || return 1 13 | ${CLI} contract new --template crypto-zombies --path ${SANDBOX} || return 1 14 | ${CLI} contract new --template empty --path ${SANDBOX} || return 1 15 | } 16 | 17 | testBuildContracts() { 18 | echo "testBuildContracts" 19 | 20 | # Improve compilation time by reusing build artifacts for Rust projects 21 | export TARGET_DIR=$(pwd)/${SANDBOX}/TARGET 22 | mkdir -p ${TARGET_DIR} 23 | 24 | ${CLI} contract build --path=${SANDBOX}/adder --target-dir=${TARGET_DIR} || return 1 25 | assertFileExists ${SANDBOX}/adder/output/adder.wasm || return 1 26 | assertFileExists ${SANDBOX}/adder/output/adder.abi.json || return 1 27 | 28 | ${CLI} contract build --path=${SANDBOX}/crypto-zombies --target-dir=${TARGET_DIR} || return 1 29 | assertFileExists ${SANDBOX}/crypto-zombies/output/crypto-zombies.wasm || return 1 30 | assertFileExists ${SANDBOX}/crypto-zombies/output/crypto-zombies.abi.json || return 1 31 | 32 | ${CLI} contract build --path=${SANDBOX}/empty --target-dir=${TARGET_DIR} || return 1 33 | assertFileExists ${SANDBOX}/empty/output/empty.wasm || return 1 34 | assertFileExists ${SANDBOX}/empty/output/empty.abi.json || return 1 35 | } 36 | 37 | testRunScenarios() { 38 | echo "testRunScenarios" 39 | ${CLI} --verbose contract test --path=${SANDBOX}/adder || return 1 40 | ${CLI} --verbose contract test --path=${SANDBOX}/empty || return 1 41 | } 42 | 43 | testWasmName() { 44 | echo "testWasmName" 45 | 46 | ${CLI} contract clean --path ${SANDBOX}/adder 47 | assertFileDoesNotExist ${SANDBOX}/adder/output/adder-2.wasm || return 1 48 | ${CLI} contract build --path=${SANDBOX}/adder --target-dir=${TARGET_DIR} --wasm-name adder-2 || return 1 49 | assertFileExists ${SANDBOX}/adder/output/adder-2.wasm || return 1 50 | assertFileExists ${SANDBOX}/adder/output/adder.abi.json || return 1 51 | } 52 | 53 | testCleanContracts() { 54 | echo "testCleanContracts" 55 | 56 | assertFileExists ${SANDBOX}/adder/output/adder.wasm || return 1 57 | assertFileExists ${SANDBOX}/adder/output/adder.abi.json || return 1 58 | ${CLI} contract clean --path ${SANDBOX}/adder || return 1 59 | assertFileDoesNotExist ${SANDBOX}/adder/output/adder.wasm || return 1 60 | assertFileDoesNotExist ${SANDBOX}/adder/output/adder.abi.json || return 1 61 | 62 | assertFileExists ${SANDBOX}/crypto-zombies/output/crypto-zombies.wasm || return 1 63 | assertFileExists ${SANDBOX}/crypto-zombies/output/crypto-zombies.abi.json || return 1 64 | ${CLI} contract clean --path ${SANDBOX}/crypto-zombies || return 1 65 | assertFileDoesNotExist ${SANDBOX}/crypto-zombies/output/crypto-zombies.wasm || return 1 66 | assertFileDoesNotExist ${SANDBOX}/crypto-zombies/output/crypto-zombies.abi.json || return 1 67 | 68 | assertFileExists ${SANDBOX}/empty/output/empty.wasm || return 1 69 | assertFileExists ${SANDBOX}/empty/output/empty.abi.json || return 1 70 | ${CLI} contract clean --path ${SANDBOX}/empty || return 1 71 | assertFileDoesNotExist ${SANDBOX}/empty/output/empty.wasm || return 1 72 | assertFileDoesNotExist ${SANDBOX}/empty/output/empty.abi.json || return 1 73 | } 74 | 75 | testVerifyContract(){ 76 | echo "testVerifyContract" 77 | 78 | nohup python3 local_verify_server.py >/dev/null 2>&1 & 79 | sleep 1 80 | 81 | query_response=$(curl -s localhost:7777/verify -X POST) 82 | 83 | command_response=$(${CLI} contract verify erd1qqqqqqqqqqqqqpgquzmh78klkqwt0p4rjys0qtp3la07gz4d396qn50nnm \ 84 | --verifier-url=http://localhost:7777 --packaged-src=testdata/dummy.json \ 85 | --pem=testdata/walletKey.pem --docker-image=multiversx/sdk-rust-contract-builder:v4.0.0) 86 | 87 | result_curl=$(echo $query_response | awk -F ": " '{ print $2 }' | awk -F'"' '{print $2}') 88 | result_cli=$(echo $command_response | awk -F ": " '{ print $2 }' | awk -F'"' '{print $2}') 89 | 90 | if [[ $result_curl == $result_cli ]]; 91 | then 92 | echo "Test passed!" 93 | else 94 | return 1 95 | fi 96 | 97 | pkill -f local_verify_server.py 98 | } 99 | 100 | testReproducibleBuild() { 101 | echo "testReproducibleBuild" 102 | 103 | wget -O ${SANDBOX}/example.zip https://github.com/multiversx/mx-reproducible-contract-build-example-sc/archive/refs/tags/v0.2.1.zip || return 1 104 | unzip ${SANDBOX}/example.zip -d ${SANDBOX} || return 1 105 | ${CLI} contract reproducible-build ${SANDBOX}/mx-reproducible-contract-build-example-sc-0.2.1 --docker-image=multiversx/sdk-rust-contract-builder:v4.1.2 --no-docker-interactive --no-docker-tty || return 1 106 | assertFileExists ${SANDBOX}/mx-reproducible-contract-build-example-sc-0.2.1/output-docker/artifacts.json || return 1 107 | } 108 | 109 | testAll() { 110 | ${CLI} config set dependencies.rust.tag ${RUST_VERSION} 111 | 112 | cleanSandbox || return 1 113 | testTrivialCommands || return 1 114 | testCreateContracts || return 1 115 | testBuildContracts || return 1 116 | testRunScenarios || return 1 117 | testCleanContracts || return 1 118 | testWasmName || return 1 119 | } 120 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_cli_deps.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from multiversx_sdk_cli.cli import main 4 | 5 | 6 | def test_deps_install_testwallets(): 7 | return_code = main(["deps", "install", "testwallets"]) 8 | assert return_code == 0 9 | 10 | 11 | def test_deps_check_testwallets(): 12 | return_code = main(["deps", "check", "testwallets"]) 13 | assert return_code == 0 14 | 15 | 16 | @pytest.mark.skip_on_windows 17 | def test_deps_install_all(): 18 | return_code = main(["deps", "install", "all"]) 19 | assert return_code == 0 20 | 21 | 22 | @pytest.mark.skip_on_windows 23 | def test_deps_check_all(): 24 | return_code = main(["deps", "check", "all"]) 25 | assert return_code == 0 26 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_cli_dns.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | from multiversx_sdk_cli.cli import main 6 | 7 | testdata_path = Path(__file__).parent / "testdata" 8 | 9 | 10 | def test_prepare_relayed_dns_register_transaction(capsys: Any): 11 | alice = testdata_path / "alice.pem" 12 | user = testdata_path / "testUser.pem" 13 | 14 | return_code = main( 15 | [ 16 | "dns", 17 | "register", 18 | "--pem", 19 | str(alice), 20 | "--name", 21 | "alice.elrond", 22 | "--nonce", 23 | "0", 24 | "--gas-limit", 25 | "15000000", 26 | "--chain", 27 | "T", 28 | "--relayer", 29 | "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", 30 | "--relayer-pem", 31 | str(user), 32 | ] 33 | ) 34 | assert not return_code 35 | 36 | output = get_output(capsys) 37 | tx = output["emittedTransaction"] 38 | data = output["emittedTransactionData"] 39 | 40 | assert tx["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" 41 | assert tx["receiver"] == "erd1qqqqqqqqqqqqqpgqf97pgqdy0tstwauxu09kszz020hp5kgqqzzsscqtww" 42 | assert tx["value"] == "0" 43 | assert tx["nonce"] == 0 44 | assert tx["gasLimit"] == 15000000 45 | assert tx["chainID"] == "T" 46 | assert tx["version"] == 2 47 | assert tx["options"] == 0 48 | assert tx["relayer"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" 49 | assert ( 50 | tx["signature"] 51 | == "25af35896b853ad13bf1adcc3e516f8d7532349b4b00e958e30a6b9c0b9c38cbe0ce712684aba91e3f91bf080f6ae89d8cdfd6d0c69701d3761346aa6c54ac0d" 52 | ) 53 | assert ( 54 | tx["relayerSignature"] 55 | == "e7b22c3f8e3cfa8f15038d3b59beabe3e4b2a0e40fdb40e57c39e762450ebe3cdf327bb66585c27e66480846b7487d5e78366959f6f09f10bb63e9b643c08f03" 56 | ) 57 | assert data == "register@616c6963652e656c726f6e64" 58 | 59 | 60 | def get_output(capsys: Any): 61 | tx = _read_stdout(capsys) 62 | return json.loads(tx) 63 | 64 | 65 | def _read_stdout(capsys: Any) -> str: 66 | stdout: str = capsys.readouterr().out.strip() 67 | return stdout 68 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_cli_localnet.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | source "./shared.sh" 4 | 5 | testStartThenStop() { 6 | pushd . 7 | 8 | cleanSandbox 9 | mkdir -p ${SANDBOX}/localnet_foo 10 | cp ./testdata/localnet_with_resolution_remote.toml ${SANDBOX}/localnet_foo/localnet.toml 11 | cd ${SANDBOX}/localnet_foo 12 | 13 | ${CLI} localnet prerequisites 14 | ${CLI} localnet build 15 | ${CLI} localnet config 16 | 17 | # Leave the localnet to run for some time, then assert for "time out error" (124). 18 | timeout 1m ${CLI} localnet start || \ 19 | test $? -eq 124 || \ 20 | echo "Timeout error expected, but something else happened." 21 | 22 | popd 23 | } 24 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_cli_validator_wallet.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | from typing import Any 4 | 5 | from multiversx_sdk import ValidatorPEM 6 | 7 | from multiversx_sdk_cli.cli import main 8 | 9 | testdata_path = Path(__file__).parent / "testdata" 10 | testdata_out_path = Path(__file__).parent / "testdata-out" 11 | 12 | 13 | def test_create_validator_wallet(): 14 | outfile = testdata_out_path / "validator.pem" 15 | outfile.unlink(missing_ok=True) 16 | 17 | return_code = main( 18 | [ 19 | "validator-wallet", 20 | "new", 21 | "--outfile", 22 | str(outfile), 23 | ] 24 | ) 25 | assert not return_code 26 | assert outfile.is_file() 27 | 28 | wallet = ValidatorPEM.from_file(outfile) 29 | assert wallet.label 30 | assert wallet.secret_key 31 | 32 | 33 | def test_validator_sign_and_verify_message(capsys: Any): 34 | message = "test" 35 | validator = testdata_path / "validator_01.pem" 36 | 37 | return_code = main( 38 | [ 39 | "validator-wallet", 40 | "sign-message", 41 | "--message", 42 | message, 43 | "--pem", 44 | str(validator), 45 | ] 46 | ) 47 | assert not return_code 48 | 49 | out = json.loads(_read_stdout(capsys)) 50 | assert out == { 51 | "address": "f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d", 52 | "message": "test", 53 | "signature": "0x1c1dc0f6ef4f7c2a335cabd1da4bf3f333902c90ad9ecff0873854453419cd092f490238fb2f1bc6bf9f89337dea188f", 54 | } 55 | 56 | # Clear the captured content 57 | capsys.readouterr() 58 | 59 | return_code = main( 60 | [ 61 | "validator-wallet", 62 | "verify-message-signature", 63 | "--pubkey", 64 | "f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d", 65 | "--message", 66 | message, 67 | "--signature", 68 | "0x1c1dc0f6ef4f7c2a335cabd1da4bf3f333902c90ad9ecff0873854453419cd092f490238fb2f1bc6bf9f89337dea188f", 69 | ] 70 | ) 71 | assert not return_code 72 | out = _read_stdout(capsys) 73 | 74 | success = "SUCCESS:" 75 | assert success in out.split() 76 | 77 | # repeate signature check with invalid signature 78 | # Clear the captured content 79 | capsys.readouterr() 80 | 81 | return_code = main( 82 | [ 83 | "validator-wallet", 84 | "verify-message-signature", 85 | "--pubkey", 86 | "f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d", 87 | "--message", 88 | message, 89 | "--signature", 90 | "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 91 | ] 92 | ) 93 | assert not return_code 94 | out = _read_stdout(capsys) 95 | 96 | success = "SUCCESS:" 97 | assert success not in out.split() 98 | 99 | fail = "FAILED:" 100 | assert fail in out.split() 101 | 102 | 103 | def test_validator_wallet_convert_to_hex_secret_key(capsys: Any): 104 | infile = testdata_path / "validator_01.pem" 105 | 106 | return_code = main( 107 | [ 108 | "validator-wallet", 109 | "convert", 110 | "--infile", 111 | str(infile), 112 | ] 113 | ) 114 | assert not return_code 115 | 116 | output = _read_stdout(capsys) 117 | lines = output.splitlines() 118 | 119 | assert ( 120 | lines[0] 121 | == "Public key: f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d" 122 | ) 123 | assert lines[1] == "Secret key: 7c19bf3a0c57cdd1fb08e4607cebaa3647d6b9261b4693f61e96e54b218d442a" 124 | 125 | 126 | def _read_stdout(capsys: Any) -> str: 127 | stdout: str = capsys.readouterr().out.strip() 128 | return stdout 129 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_contracts.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from Cryptodome.Hash import keccak 5 | from multiversx_sdk import Account, Address, TransactionsFactoryConfig 6 | 7 | from multiversx_sdk_cli.contract_verification import _create_request_signature 8 | from multiversx_sdk_cli.contracts import SmartContract 9 | 10 | logging.basicConfig(level=logging.INFO) 11 | 12 | testdata_folder = Path(__file__).parent / "testdata" 13 | 14 | 15 | def test_playground_keccak(): 16 | hexhash = keccak.new(digest_bits=256).update(b"").hexdigest() 17 | assert hexhash == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470" 18 | 19 | 20 | def test_contract_verification_create_request_signature(): 21 | account = Account.new_from_pem(file_path=testdata_folder / "walletKey.pem") 22 | contract_address = Address.from_bech32("erd1qqqqqqqqqqqqqpgqeyj9g344pqguukajpcfqz9p0rfqgyg4l396qespdck") 23 | request_payload = b"test" 24 | signature = _create_request_signature(account, contract_address, request_payload) 25 | 26 | assert ( 27 | signature.hex() 28 | == "30111258cc42ea08e0c6a3e053cc7086a88d614b8b119a244904e9a19896c73295b2fe5c520a1cb07cfe20f687deef9f294a0a05071e85c78a70a448ea5f0605" 29 | ) 30 | 31 | 32 | def test_prepare_args_for_factories(): 33 | sc = SmartContract(TransactionsFactoryConfig("mock")) 34 | args = [ 35 | "0x5", 36 | "123", 37 | "false", 38 | "true", 39 | "str:test-string", 40 | "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", 41 | ] 42 | 43 | arguments = sc._prepare_args_for_factory(args) 44 | assert arguments[0].get_payload() == b"\x05" 45 | assert arguments[1].get_payload() == 123 46 | assert arguments[2].get_payload() is False 47 | assert arguments[3].get_payload() is True 48 | assert arguments[4].get_payload() == "test-string" 49 | assert ( 50 | arguments[5].get_payload() 51 | == Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th").get_public_key() 52 | ) 53 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_playground_proxy.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import requests 3 | 4 | 5 | class TestPlaygroundProxy: 6 | @pytest.mark.skip("manual run only") 7 | def test_do_request(self): 8 | # use a valid proxy address 9 | url = "http://localhost:8001" 10 | # use a valid account address 11 | address = "78570f77898db9f59b8377192f3e657eac484bb1b4ed245e594bbc67d894c5ad" 12 | response = requests.get(url + "/address/" + address + "/nonce") 13 | print("response status code " + str(response.status_code)) 14 | print(response.json()) 15 | assert response is not None 16 | 17 | @pytest.mark.skip("manual run only") 18 | def test_do_request_node_status(self): 19 | # use a valid proxy address 20 | url = "http://localhost:8001" 21 | # use a valid shard id 22 | shard_id = "0" 23 | 24 | response = requests.get(f"{url}/network/status/{shard_id}") 25 | print("response status code " + str(response.status_code)) 26 | print(response.json()) 27 | assert response is not None 28 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_proxy.py: -------------------------------------------------------------------------------- 1 | from multiversx_sdk import Address, ProxyNetworkProvider 2 | 3 | from multiversx_sdk_cli.cli import main 4 | from multiversx_sdk_cli.config import get_config_for_network_providers 5 | 6 | 7 | def test_sync_nonce(): 8 | account = Address.new_from_bech32("erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th") 9 | config = get_config_for_network_providers() 10 | proxy = ProxyNetworkProvider("https://testnet-api.multiversx.com", config=config) 11 | nonce = proxy.get_account(account).nonce 12 | assert True if nonce else False 13 | 14 | 15 | def test_query_contract(): 16 | result = main( 17 | [ 18 | "contract", 19 | "query", 20 | "erd1qqqqqqqqqqqqqpgq6qr0w0zzyysklfneh32eqp2cf383zc89d8sstnkl60", 21 | "--function", 22 | "getSum", 23 | "--proxy", 24 | "https://devnet-api.multiversx.com", 25 | ] 26 | ) 27 | assert False if result else True 28 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_rust.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from pathlib import Path 3 | 4 | from multiversx_sdk_cli import utils 5 | 6 | logging.basicConfig(level=logging.INFO) 7 | 8 | 9 | class TestProjectRust: 10 | def test_set_up(self): 11 | self.testdata = Path(__file__).parent.joinpath("testdata") 12 | self.testdata_out = Path(__file__).parent.joinpath("testdata-out") 13 | utils.ensure_folder(self.testdata_out) 14 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_sign.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | from multiversx_sdk_cli.cli import main 5 | 6 | 7 | def test_sign_tx(): 8 | parent = Path(__file__).parent 9 | unsigned_transaction = parent / "testdata" / "transaction.json" 10 | signed_transaction = parent / "testdata-out" / "signed_transaction.json" 11 | expected_signature = "7b0fa3bd477a9aacdfd8d6b41628e525afbbc94b4b56c2a30a10f78514c2f6558b27eef701633481f1ef54b62697c91e9dc06cc6d2038bd13cf9557467142005" 12 | 13 | main( 14 | [ 15 | "tx", 16 | "sign", 17 | "--pem", 18 | f"{parent}/testdata/testUser.pem", 19 | "--infile", 20 | f"{unsigned_transaction}", 21 | "--outfile", 22 | f"{signed_transaction}", 23 | ] 24 | ) 25 | 26 | with open(signed_transaction) as f: 27 | signed_tx = json.load(f) 28 | 29 | assert signed_tx["emittedTransaction"]["signature"] == expected_signature 30 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/test_testnet.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from typing import Any, Dict 4 | 5 | from multiversx_sdk_cli.localnet.config_root import ConfigRoot 6 | from multiversx_sdk_cli.localnet.config_software import SoftwareResolution 7 | 8 | sys.path = [os.getcwd() + "/.."] + sys.path 9 | 10 | 11 | def test_override_config() -> None: 12 | config = ConfigRoot() 13 | 14 | # Check a few default values 15 | assert config.general.rounds_per_epoch == 100 16 | assert config.general.round_duration_milliseconds == 6000 17 | assert config.metashard.consensus_size == 1 18 | assert config.networking.port_proxy == 7950 19 | assert config.software.mx_chain_go.resolution == SoftwareResolution.Remote 20 | assert ( 21 | config.software.mx_chain_go.archive_url 22 | == "https://github.com/multiversx/mx-chain-go/archive/refs/heads/master.zip" 23 | ) 24 | 25 | # Now partly override the config 26 | config_patch: Dict[str, Any] = dict() 27 | config_patch["general"] = { 28 | "rounds_per_epoch": 200, 29 | "round_duration_milliseconds": 4000, 30 | } 31 | config_patch["metashard"] = { 32 | "consensus_size": 2, 33 | } 34 | config_patch["networking"] = { 35 | "port_proxy": 7951, 36 | } 37 | config_patch["software"] = { 38 | "mx_chain_go": {"archive_url": "https://github.com/multiversx/mx-chain-go/archive/refs/tags/v1.5.1.zip"} 39 | } 40 | 41 | config.override(config_patch) 42 | 43 | # Check the overridden values 44 | assert config.general.rounds_per_epoch == 200 45 | assert config.general.round_duration_milliseconds == 4000 46 | assert config.metashard.consensus_size == 2 47 | assert config.networking.port_proxy == 7951 48 | assert config.software.mx_chain_go.resolution == SoftwareResolution.Remote 49 | assert ( 50 | config.software.mx_chain_go.archive_url 51 | == "https://github.com/multiversx/mx-chain-go/archive/refs/tags/v1.5.1.zip" 52 | ) 53 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/adder.abi.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildInfo": { 3 | "rustc": { 4 | "version": "1.79.0", 5 | "commitHash": "129f3b9964af4d4a709d1383930ade12dfe7c081", 6 | "commitDate": "2024-06-10", 7 | "channel": "Stable", 8 | "short": "rustc 1.79.0 (129f3b996 2024-06-10)" 9 | }, 10 | "contractCrate": { 11 | "name": "adder", 12 | "version": "0.0.0", 13 | "gitVersion": "v0.45.2.1-reproducible-337-g5478c6e" 14 | }, 15 | "framework": { 16 | "name": "multiversx-sc", 17 | "version": "0.51.1" 18 | } 19 | }, 20 | "docs": [ 21 | "One of the simplest smart contracts possible,", 22 | "it holds a single variable in storage, which anyone can increment." 23 | ], 24 | "name": "Adder", 25 | "constructor": { 26 | "inputs": [ 27 | { 28 | "name": "initial_value", 29 | "type": "BigUint" 30 | } 31 | ], 32 | "outputs": [] 33 | }, 34 | "upgradeConstructor": { 35 | "inputs": [ 36 | { 37 | "name": "initial_value", 38 | "type": "BigUint" 39 | } 40 | ], 41 | "outputs": [] 42 | }, 43 | "endpoints": [ 44 | { 45 | "name": "getSum", 46 | "mutability": "readonly", 47 | "inputs": [], 48 | "outputs": [ 49 | { 50 | "type": "BigUint" 51 | } 52 | ] 53 | }, 54 | { 55 | "docs": [ 56 | "Add desired amount to the storage variable." 57 | ], 58 | "name": "add", 59 | "mutability": "mutable", 60 | "inputs": [ 61 | { 62 | "name": "value", 63 | "type": "BigUint" 64 | } 65 | ], 66 | "outputs": [] 67 | } 68 | ], 69 | "esdtAttributes": [], 70 | "hasCallback": false, 71 | "types": {} 72 | } 73 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/adder.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-py-cli/e2da34bd5cb490ad4d99af4dfcffa54e44c1d4af/multiversx_sdk_cli/tests/testdata/adder.wasm -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/alice.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "id": "0dc10c02-b59b-4bac-9710-6b2cfa4284ba", 4 | "address": "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", 5 | "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", 6 | "crypto": { 7 | "ciphertext": "4c41ef6fdfd52c39b1585a875eb3c86d30a315642d0e35bb8205b6372c1882f135441099b11ff76345a6f3a930b5665aaf9f7325a32c8ccd60081c797aa2d538", 8 | "cipherparams": { 9 | "iv": "033182afaa1ebaafcde9ccc68a5eac31" 10 | }, 11 | "cipher": "aes-128-ctr", 12 | "kdf": "scrypt", 13 | "kdfparams": { 14 | "dklen": 32, 15 | "salt": "4903bd0e7880baa04fc4f886518ac5c672cdc745a6bd13dcec2b6c12e9bffe8d", 16 | "n": 4096, 17 | "r": 8, 18 | "p": 1 19 | }, 20 | "mac": "5b4a6f14ab74ba7ca23db6847e28447f0e6a7724ba9664cf425df707a84f5a8b" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/alice.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- 2 | NDEzZjQyNTc1ZjdmMjZmYWQzMzE3YTc3ODc3MTIxMmZkYjgwMjQ1ODUwOTgxZTQ4 3 | YjU4YTRmMjVlMzQ0ZThmOTAxMzk0NzJlZmY2ODg2NzcxYTk4MmYzMDgzZGE1ZDQy 4 | MWYyNGMyOTE4MWU2Mzg4ODIyOGRjODFjYTYwZDY5ZTE= 5 | -----END PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- 6 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/aliceWithKindSecretKey.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "id": "0dc10c02-b59b-4bac-9710-6b2cfa4284ba", 4 | "address": "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1", 5 | "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", 6 | "kind": "secretKey", 7 | "crypto": { 8 | "ciphertext": "4c41ef6fdfd52c39b1585a875eb3c86d30a315642d0e35bb8205b6372c1882f135441099b11ff76345a6f3a930b5665aaf9f7325a32c8ccd60081c797aa2d538", 9 | "cipherparams": { 10 | "iv": "033182afaa1ebaafcde9ccc68a5eac31" 11 | }, 12 | "cipher": "aes-128-ctr", 13 | "kdf": "scrypt", 14 | "kdfparams": { 15 | "dklen": 32, 16 | "salt": "4903bd0e7880baa04fc4f886518ac5c672cdc745a6bd13dcec2b6c12e9bffe8d", 17 | "n": 4096, 18 | "r": 8, 19 | "p": 1 20 | }, 21 | "mac": "5b4a6f14ab74ba7ca23db6847e28447f0e6a7724ba9664cf425df707a84f5a8b" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/call_multisig_propose_batch_args.json: -------------------------------------------------------------------------------- 1 | [ 2 | [ 3 | { 4 | "__discriminant__": 5, 5 | "0": { 6 | "to": { 7 | "bech32": "erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3" 8 | }, 9 | "egld_amount": 1000000000000000000, 10 | "endpoint_name": "add", 11 | "arguments": [ 12 | { 13 | "hex": "07" 14 | } 15 | ], 16 | "opt_gas_limit": 15000000 17 | } 18 | } 19 | ] 20 | ] 21 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/deploy_multisig_args.json: -------------------------------------------------------------------------------- 1 | [ 2 | 2, 3 | [ 4 | { 5 | "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" 6 | }, 7 | { 8 | "bech32": "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" 9 | } 10 | 11 | ] 12 | ] 13 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | log_level = "*:DEBUG" 3 | rounds_per_epoch = 40 4 | round_duration_milliseconds = 3000 5 | 6 | [software.mx_chain_go] 7 | resolution = "local" 8 | local_path = "~/multiversx-sdk/sandbox/mx-chain-go" 9 | 10 | [software.mx_chain_proxy_go] 11 | resolution = "local" 12 | local_path = "~/multiversx-sdk/sandbox/mx-chain-proxy-go" 13 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml: -------------------------------------------------------------------------------- 1 | [general] 2 | log_level = "*:DEBUG" 3 | rounds_per_epoch = 40 4 | round_duration_milliseconds = 3000 5 | 6 | [software.mx_chain_go] 7 | resolution = "remote" 8 | archive_url = "https://github.com/multiversx/mx-chain-go/archive/refs/heads/master.zip" 9 | 10 | [software.mx_chain_proxy_go] 11 | resolution = "remote" 12 | archive_url = "https://github.com/multiversx/mx-chain-proxy-go/archive/refs/heads/master.zip" 13 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/mnemonic.txt: -------------------------------------------------------------------------------- 1 | moral volcano peasant pass circle pen over picture flat shop clap goat never lyrics gather prepare woman film husband gravity behind test tiger improve -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/multipleValidatorsKeys.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- 2 | N2MxOWJmM2EwYzU3Y2RkMWZiMDhlNDYwN2NlYmFhMzY0N2Q2YjkyNjFiNDY5M2Y2 3 | MWU5NmU1NGIyMThkNDQyYQ== 4 | -----END PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- 5 | -----BEGIN PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- 6 | MzAzNGIxZDU4NjI4YTg0Mjk4NGRhMGM3MGRhMGI1YTI1MWViYjJhZWJmNTFhZmM1 7 | YjU4NmUyODM5YjVlNTI2Mw== 8 | -----END PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- 9 | -----BEGIN PRIVATE KEY for e5dc552b4b170cdec4405ff8f9af20313bf0e2756d06c35877b6fbcfa6b354a7b3e2d439ea87999befb09a8fa1b3f014e57ec747bf738c4199338fcd4a87b373dd62f5c8329f1f5f245956bbb06685596a2e83dc38befa63e4a2b5c4ce408506----- 10 | ZGU3ZTFiMzg1ZWRiYjBlMWU4ZjlmYzI1ZDkxYmQ4ZWVkNzFhMWRhN2NhYWI3MzJl 11 | NmI0N2E0ODA0MmQ4NTIzZA== 12 | -----END PRIVATE KEY for e5dc552b4b170cdec4405ff8f9af20313bf0e2756d06c35877b6fbcfa6b354a7b3e2d439ea87999befb09a8fa1b3f014e57ec747bf738c4199338fcd4a87b373dd62f5c8329f1f5f245956bbb06685596a2e83dc38befa63e4a2b5c4ce408506----- 13 | -----BEGIN PRIVATE KEY for 12773304cb718250edd89770cedcbf675ccdb7fe2b30bd3185ca65ffa0d516879768ed03f92e41a6e5bc5340b78a9d02655e3b727c79730ead791fb68eaa02b84e1be92a816a9604a1ab9a6d3874b638487e2145239438a4bafac3889348d405----- 14 | OGViZWIwN2QyOTZhZDI1Mjk0MDBiNDA2ODdhNzQxYTEzNWY4MzU3Zjc5ZjM5ZmNi 15 | Mjg5NGE2Zjk3MDNhNTgxNg== 16 | -----END PRIVATE KEY for 12773304cb718250edd89770cedcbf675ccdb7fe2b30bd3185ca65ffa0d516879768ed03f92e41a6e5bc5340b78a9d02655e3b727c79730ead791fb68eaa02b84e1be92a816a9604a1ab9a6d3874b638487e2145239438a4bafac3889348d405----- -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/multiple_addresses.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- 2 | NDEzZjQyNTc1ZjdmMjZmYWQzMzE3YTc3ODc3MTIxMmZkYjgwMjQ1ODUwOTgxZTQ4 3 | YjU4YTRmMjVlMzQ0ZThmOTAxMzk0NzJlZmY2ODg2NzcxYTk4MmYzMDgzZGE1ZDQy 4 | MWYyNGMyOTE4MWU2Mzg4ODIyOGRjODFjYTYwZDY5ZTE= 5 | -----END PRIVATE KEY for erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th----- 6 | -----BEGIN PRIVATE KEY for erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx----- 7 | YjhjYTZmODIwM2ZiNGI1NDVhOGU4M2M1Mzg0ZGEwMzNjNDE1ZGIxNTViNTNmYjVi 8 | OGViYTdmZjVhMDM5ZDYzOTgwNDlkNjM5ZTVhNjk4MGQxY2QyMzkyYWJjY2U0MTAy 9 | OWNkYTc0YTE1NjM1MjNhMjAyZjA5NjQxY2MyNjE4Zjg= 10 | -----END PRIVATE KEY for erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx----- 11 | -----BEGIN PRIVATE KEY for erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8----- 12 | ZTI1M2E1NzFjYTE1M2RjMmFlZTg0NTgxOWY3NGJjYzk3NzNiMDU4NmVkZWFkMTVh 13 | OTRjYjcyMzVhNTAyNzQzNmIyYTExNTU1Y2U1MjFlNDk0NGUwOWFiMTc1NDlkODVi 14 | NDg3ZGNkMjZjODRiNTAxN2EzOWUzMWEzNjcwODg5YmE= 15 | -----END PRIVATE KEY for erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8----- 16 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/multisig.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/multiversx/mx-sdk-py-cli/e2da34bd5cb490ad4d99af4dfcffa54e44c1d4af/multiversx_sdk_cli/tests/testdata/multisig.wasm -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/testUser.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "kind": "secretKey", 4 | "id": "98054be9-4bca-4455-beac-8a3b2c7ed7e5", 5 | "address": "c0006edaaee4fd479f2f248b341eb11eaecaec4d7dee190619958332bba5200f", 6 | "bech32": "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", 7 | "crypto": { 8 | "ciphertext": "4c5af3b188120e22eba65ed7c87804f0f8e05348e1545d09f4074b3c277475999f42b5bc9533b89b801db832928fc9891b400962aa332b7d9da0dceac6e82dd2", 9 | "cipherparams": { 10 | "iv": "7d9ed45a9596950921644c90d9c6f8cf" 11 | }, 12 | "cipher": "aes-128-ctr", 13 | "kdf": "scrypt", 14 | "kdfparams": { 15 | "dklen": 32, 16 | "salt": "5a70a17c2d591de7377e19fb1cf7c6bb9e6e82f9fdfb98c2d8f618ed1e4f941c", 17 | "n": 4096, 18 | "r": 8, 19 | "p": 1 20 | }, 21 | "mac": "81a4844b7d051776096f17ea4028b81c9c2884b13a19fc6e3f44ffe4a7e52b07" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/testUser.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5----- 2 | ZTdjYjVjMWM0OTg3ZjYwOWNiZDI1MDQxNjIwZjAwZmQ0ZjU4YmFhNzljNmU4ZDI4 3 | NTc2OWZlYmVhYTQwNmVlMGMwMDA2ZWRhYWVlNGZkNDc5ZjJmMjQ4YjM0MWViMTFl 4 | YWVjYWVjNGQ3ZGVlMTkwNjE5OTU4MzMyYmJhNTIwMGY= 5 | -----END PRIVATE KEY for erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5----- 6 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/testUser2.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "kind": "secretKey", 4 | "id": "7db5c2ac-23cf-485d-bbf1-e8390300c62e", 5 | "address": "84370c15ea571b8385aef3ad1b99deac8657bcc851d8e08a22d3d190eb49ec3b", 6 | "bech32": "erd1ssmsc9022udc8pdw7wk3hxw74jr900xg28vwpz3z60gep66fasasl2nkm4", 7 | "crypto": { 8 | "ciphertext": "85aecdc9c7d7c9774f90629c0dc0940ae3aab0bcdeddca521ab6489a62e9d9f5b5b3310e91f849880c5d999ed3ef741b79667df6dd09981b7b494d0b8076d1ea", 9 | "cipherparams": { 10 | "iv": "e32fac328e2838b446e3675119b751ad" 11 | }, 12 | "cipher": "aes-128-ctr", 13 | "kdf": "scrypt", 14 | "kdfparams": { 15 | "dklen": 32, 16 | "salt": "84cf1d7cdf62be5dcfe12ad75fde6ac25707bf762915c047bdc6d94188fcbb0c", 17 | "n": 4096, 18 | "r": 8, 19 | "p": 1 20 | }, 21 | "mac": "8894b7c371ca5557fd69491f7af82e076da4f07e555db0f5bc44d67272ddf09e" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/testUser2.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for erd1ssmsc9022udc8pdw7wk3hxw74jr900xg28vwpz3z60gep66fasasl2nkm4----- 2 | YmEwODdlODNlM2RiOGU1NDZmYzA5M2NmY2M0NmNmMWMyMDVkZTUxYmZhZGRhYzA0 3 | NWEzODY0ZWQ3YWM5OGE3Njg0MzcwYzE1ZWE1NzFiODM4NWFlZjNhZDFiOTlkZWFj 4 | ODY1N2JjYzg1MWQ4ZTA4YTIyZDNkMTkwZWI0OWVjM2I= 5 | -----END PRIVATE KEY for erd1ssmsc9022udc8pdw7wk3hxw74jr900xg28vwpz3z60gep66fasasl2nkm4----- 6 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/transaction.json: -------------------------------------------------------------------------------- 1 | { 2 | "emittedTransaction": { 3 | "nonce": 7, 4 | "value": "1000000000000000000", 5 | "receiver": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", 6 | "sender": "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", 7 | "gasPrice": 1000000000, 8 | "gasLimit": 74000, 9 | "data": "dGVzdCB0cmFuc2FjdGlvbg==", 10 | "chainID": "T", 11 | "version": 2, 12 | "signature": "" 13 | }, 14 | "emittedTransactionData": "test transaction", 15 | "emittedTransactionHash": "" 16 | } 17 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/upgrade_multisig_args.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "bech32": "erd1qqqqqqqqqqqqqpgqpg2tnjelx3s3dhkksqhjavprtgmt9xt4d8ss9al8m4" 4 | }, 5 | 0, 6 | { 7 | "bech32": "erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3" 8 | }, 9 | { 10 | "hex": "0500" 11 | }, 12 | [ 13 | 0 14 | ] 15 | ] 16 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/validator_01.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- 2 | N2MxOWJmM2EwYzU3Y2RkMWZiMDhlNDYwN2NlYmFhMzY0N2Q2YjkyNjFiNDY5M2Y2 3 | MWU5NmU1NGIyMThkNDQyYQ== 4 | -----END PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- 5 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/validator_02.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- 2 | MzAzNGIxZDU4NjI4YTg0Mjk4NGRhMGM3MGRhMGI1YTI1MWViYjJhZWJmNTFhZmM1 3 | YjU4NmUyODM5YjVlNTI2Mw== 4 | -----END PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- 5 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/validators.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- 2 | N2NmZjk5YmQ2NzE1MDJkYjdkMTViYzhhYmMwYzlhODA0ZmI5MjU0MDZmYmRkNTBm 3 | MWU0YzE3YTRjZDc3NDI0Nw== 4 | -----END PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- 5 | -----BEGIN PRIVATE KEY for 78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d----- 6 | ODA4NWJhMWQ3ZjdjM2RiOTM4YWQ3MDU5NWEyYmRhYjA5NjQ0ZjFlYzM4MDNiZTE3 7 | MWMzM2YxNGJjODBkNGUzYg== 8 | -----END PRIVATE KEY for 78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d----- 9 | -----BEGIN PRIVATE KEY for 7188b234a8bf834f2e6258012aa09a2ab93178ffab9c789480275f61fe02cd1b9a58ddc63b79a73abea9e2b7ac5cac0b0d4324eff50aca2f0ec946b9ae6797511fa3ce461b57e77129cba8ab3b51147695d4ce889cbe67905f6586b4e4f22491----- 10 | ZjFkYjBkMjE4NmJiZDEzOGQ4MTk5MzM3MDJlMmMxMmFkNDk5MTI0YzQ3N2Q0OGM3 11 | ZDM3MTM2N2E3MWZmMmM1Zg== 12 | -----END PRIVATE KEY for 7188b234a8bf834f2e6258012aa09a2ab93178ffab9c789480275f61fe02cd1b9a58ddc63b79a73abea9e2b7ac5cac0b0d4324eff50aca2f0ec946b9ae6797511fa3ce461b57e77129cba8ab3b51147695d4ce889cbe67905f6586b4e4f22491----- 13 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/validators_ci.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- 2 | N2NmZjk5YmQ2NzE1MDJkYjdkMTViYzhhYmMwYzlhODA0ZmI5MjU0MDZmYmRkNTBm 3 | MWU0YzE3YTRjZDc3NDI0Nw== 4 | -----END PRIVATE KEY for e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208----- 5 | -----BEGIN PRIVATE KEY for 78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d----- 6 | ODA4NWJhMWQ3ZjdjM2RiOTM4YWQ3MDU5NWEyYmRhYjA5NjQ0ZjFlYzM4MDNiZTE3 7 | MWMzM2YxNGJjODBkNGUzYg== 8 | -----END PRIVATE KEY for 78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d----- 9 | -----BEGIN PRIVATE KEY for 7188b234a8bf834f2e6258012aa09a2ab93178ffab9c789480275f61fe02cd1b9a58ddc63b79a73abea9e2b7ac5cac0b0d4324eff50aca2f0ec946b9ae6797511fa3ce461b57e77129cba8ab3b51147695d4ce889cbe67905f6586b4e4f22491----- 10 | ZjFkYjBkMjE4NmJiZDEzOGQ4MTk5MzM3MDJlMmMxMmFkNDk5MTI0YzQ3N2Q0OGM3 11 | ZDM3MTM2N2E3MWZmMmM1Zg== 12 | -----END PRIVATE KEY for 7188b234a8bf834f2e6258012aa09a2ab93178ffab9c789480275f61fe02cd1b9a58ddc63b79a73abea9e2b7ac5cac0b0d4324eff50aca2f0ec946b9ae6797511fa3ce461b57e77129cba8ab3b51147695d4ce889cbe67905f6586b4e4f22491----- 13 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/validators_file.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- 2 | N2MxOWJmM2EwYzU3Y2RkMWZiMDhlNDYwN2NlYmFhMzY0N2Q2YjkyNjFiNDY5M2Y2 3 | MWU5NmU1NGIyMThkNDQyYQ== 4 | -----END PRIVATE KEY for f8910e47cf9464777c912e6390758bb39715fffcb861b184017920e4a807b42553f2f21e7f3914b81bcf58b66a72ab16d97013ae1cff807cefc977ef8cbf116258534b9e46d19528042d16ef8374404a89b184e0a4ee18c77c49e454d04eae8d----- 5 | -----BEGIN PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- 6 | MzAzNGIxZDU4NjI4YTg0Mjk4NGRhMGM3MGRhMGI1YTI1MWViYjJhZWJmNTFhZmM1 7 | YjU4NmUyODM5YjVlNTI2Mw== 8 | -----END PRIVATE KEY for 1b4e60e6d100cdf234d3427494dac55fbac49856cadc86bcb13a01b9bb05a0d9143e86c186c948e7ae9e52427c9523102efe9019a2a9c06db02993f2e3e6756576ae5a3ec7c235d548bc79de1a6990e1120ae435cb48f7fc436c9f9098b92a0d----- 9 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/walletKey.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY for erd1sjsk3n2d0krq3pyxxtgf0q7j3t56sgusqaujj4n82l39t9h7jers6gslr4----- 2 | MWY0ZGQ4YjdkMThiNWQwNzg1YzlkMDgwMmVjMTRkNTUzZGJhMzU2ODEyYjg1Yzdl 3 | MzQxNDM3MzM4ODQ3MjAxMDg0YTE2OGNkNGQ3ZDg2MDg4NDg2MzJkMDk3ODNkMjhh 4 | ZTlhODIzOTAwNzc5Mjk1NjY3NTdlMjU1OTZmZTk2NDc= 5 | -----END PRIVATE KEY for erd1sjsk3n2d0krq3pyxxtgf0q7j3t56sgusqaujj4n82l39t9h7jers6gslr4----- 6 | -----BEGIN PRIVATE KEY for erd10536tc3s886yqxtln74u6mztuwl5gy9k9gp8fttxda0klgxg979srtg5wt----- 7 | MjU2NWRiYmRiNjIzMDFlNGM3YjEyYjhhNDFjZDNiMmZiZDdhZTY4N2M4ZDk3NDE5 8 | MzdhYTQ4Y2YyNDZhZWIyNTdkMjNhNWUyMzAzOWY0NDAxOTdmOWZhYmNkNmM0YmUz 9 | YmY0NDEwYjYyYTAyNzRhZDY2NmY1ZjZmYTBjODJmOGI= 10 | -----END PRIVATE KEY for erd10536tc3s886yqxtln74u6mztuwl5gy9k9gp8fttxda0klgxg979srtg5wt----- 11 | -----BEGIN PRIVATE KEY for erd1n230jlgfepdvf28vqt3zeawexg2jhvxqxjuqdfsss0xc62xcqcps9k54ag----- 12 | MDhkZTY5ZDM5OGY0YTVmZmRjZTBmMWE4NTY5NzA0ZGJjOGI1OGFhZjdiYTNlNzI2 13 | MTM0ZTMyZjFlOGJmMDRhZDlhYTJmOTdkMDljODVhYzRhOGVjMDJlMjJjZjVkOTMy 14 | MTUyYmIwYzAzNGI4MDZhNjEwODNjZDhkMjhkODA2MDM= 15 | -----END PRIVATE KEY for erd1n230jlgfepdvf28vqt3zeawexg2jhvxqxjuqdfsss0xc62xcqcps9k54ag----- 16 | -----BEGIN PRIVATE KEY for erd143907zxv0ujxr9q4mda7rmczn2xwhmqn7p9lfz666z8hd2lcks2szt5yql----- 17 | NGQ5ZGNjMWMwOWE2ZDAwYzRjOWEzODllNjYyZDlmZTI2ZTBiZjRjODU5Nzc2ZDRk 18 | MzM4YjVhOWM0MWZjMTJkNGFjNGFmZjA4Y2M3ZjI0NjE5NDE1ZGI3YmUxZWYwMjlh 19 | OGNlYmVjMTNmMDRiZjQ4YjVhZDA4Zjc2YWJmOGI0MTU= 20 | -----END PRIVATE KEY for erd143907zxv0ujxr9q4mda7rmczn2xwhmqn7p9lfz666z8hd2lcks2szt5yql----- 21 | -----BEGIN PRIVATE KEY for erd1ky4q44svhyac264mfh3em40f4zxl9jpwgxcjy2ygw07unkac97eq5y5ewj----- 22 | YTRjOTZkZDdiMjljNjhjMGY5N2NlNzQ4Y2VlY2ViOWMxYTNmYTJhZWFlMzYxMjU0 23 | N2M1MTk5ZTVmZmU1NzMxZmIxMmEwYWQ2MGNiOTNiODU2YWJiNGRlMzlkZDVlOWE4 24 | OGRmMmM4MmU0MWIxMjIyODg4NzNmZGM5ZGJiODJmYjI= 25 | -----END PRIVATE KEY for erd1ky4q44svhyac264mfh3em40f4zxl9jpwgxcjy2ygw07unkac97eq5y5ewj----- 26 | -----BEGIN PRIVATE KEY for erd1lxyf002vjzfkahhmls0q5p7y0flykg22pf80ydau3cmfvrcvscest8efeg----- 27 | MTBjMDkwY2ExNGM0MDk0Y2VkZGRhZDcwMDg1M2ZjMDBjZjI3ZmRiZDQwNjI3ZWU0 28 | YzlhOWYzZmExMzNjMzNkN2Y5ODg5N2JkNGM5MDkzNmVkZWZiZmMxZTBhMDdjNDdh 29 | N2U0YjIxNGEwYTRlZjIzN2JjOGUzNjk2MGYwYzg2MzM= 30 | -----END PRIVATE KEY for erd1lxyf002vjzfkahhmls0q5p7y0flykg22pf80ydau3cmfvrcvscest8efeg----- 31 | -----BEGIN PRIVATE KEY for erd1vqlmef5zvqy5p2rsna2qvxp7n8cddm9z84zk4ee30eefr8kz8rkqf0tkv7----- 32 | ZTUzNGMxNmYwYjM0YzVkMzdiODk1M2NjMzJjODU0OWU5YzYwMTViOGM1ZGRkYmEy 33 | YjM1ZTZmNjY5MzYxNzUxNzYwM2ZiY2E2ODI2MDA5NDBhODcwOWY1NDA2MTgzZTk5 34 | ZjBkNmVjYTIzZDQ1NmFlNzMxN2U3MjkxOWVjMjM4ZWM= 35 | -----END PRIVATE KEY for erd1vqlmef5zvqy5p2rsna2qvxp7n8cddm9z84zk4ee30eefr8kz8rkqf0tkv7----- 36 | -----BEGIN PRIVATE KEY for erd153c989k5gj0auums07lgqlgt2a4fdr7g6zl7ms77zjwm94hyvces48x77w----- 37 | NjlmYzhmODBjNmJjNzMxZGI4NDI3MmJkMTQzNmU1OGUyOTNmZWE5OGQ0NDZhODUw 38 | Mzk2YzU4ZWFiMjQyY2VmOWE0NzA1Mzk2ZDQ0NDlmZGU3MzcwN2ZiZTgwN2QwYjU3 39 | NmE5NjhmYzhkMGJmZWRjM2RlMTQ5ZGIyZDZlNDY2MzM= 40 | -----END PRIVATE KEY for erd153c989k5gj0auums07lgqlgt2a4fdr7g6zl7ms77zjwm94hyvces48x77w----- 41 | -----BEGIN PRIVATE KEY for erd1enyk3jc3kpyg8ltah86h8erlqna3suxlm34d2j4enq3cwrnp55xsk4ntnu----- 42 | ZTVlZjA3ZTQ1NWQ0NDE5ODM0MDc2ZTkwYzEyZThlMTEyNzkzM2EwYWEyYWJhZmNh 43 | MGMyZjMzNzRkODcxZDIxOWNjYzk2OGNiMTFiMDQ4ODNmZDdkYjlmNTczZTQ3ZjA0 44 | ZmIxODcwZGZkYzZhZDU0YWI5OTgyMzg3MGU2MWE1MGQ= 45 | -----END PRIVATE KEY for erd1enyk3jc3kpyg8ltah86h8erlqna3suxlm34d2j4enq3cwrnp55xsk4ntnu----- 46 | -----BEGIN PRIVATE KEY for erd1s5yg4089zsyz0375kh6ycd0vrt5ysr88277wl5cr2lxfdk6ujz5qq4de6a----- 47 | YmUxYzcwZDI1YjgwYzBiMTQ4OGFkZTIzN2E4ODM3MGUwYWRhODE2NjY1YWUxZjNk 48 | YzE1ZjViYWM4YTk4NWIxMjg1MDg4YWJjZTUxNDA4MjdjN2Q0YjVmNDRjMzVlYzFh 49 | ZTg0ODBjZTc1N2JjZWZkMzAzNTdjYzk2ZGI1YzkwYTg= 50 | -----END PRIVATE KEY for erd1s5yg4089zsyz0375kh6ycd0vrt5ysr88277wl5cr2lxfdk6ujz5qq4de6a----- 51 | -----BEGIN PRIVATE KEY for erd10l0dx8ysppde92q759ry9e0hlu0n6xxy6lc3thwpyrxect6s8aaqczmx4l----- 52 | NTU5YWNjNjE2MGE2ZDMxZDk5Zjg3MmY0ODFkYzYzMjA5MzhmOTNiNjU1YjViZGE3 53 | OGUzMDI3OTQ3ZGE1YmFjNjdmZGVkMzFjOTAwODViOTJhODFlYTE0NjQyZTVmN2Zm 54 | MWYzZDE4YzRkN2YxMTVkZGMxMjBjZDljMmY1MDNmN2E= 55 | -----END PRIVATE KEY for erd10l0dx8ysppde92q759ry9e0hlu0n6xxy6lc3thwpyrxect6s8aaqczmx4l----- 56 | -----BEGIN PRIVATE KEY for erd1w2wytdsy6xdtdhgeajjps4g5vs860juap2hqh0yy8u09hdfdqz0sm20wgf----- 57 | MTcwZjgwMjlmZmM4OWM5MTlhOGE5ODAyYzE1Y2FkNGM2NGVjYWQ4NjEzNzE1NzVj 58 | NDhmNTJkOWYwNzViYTIxMzcyOWM0NWI2MDRkMTlhYjZkZDE5ZWNhNDE4NTUxNDY0 59 | MGZhN2NiOWQwYWFlMGJiYzg0M2YxZTViYjUyZDAwOWY= 60 | -----END PRIVATE KEY for erd1w2wytdsy6xdtdhgeajjps4g5vs860juap2hqh0yy8u09hdfdqz0sm20wgf----- 61 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/tests/testdata/withDummyMnemonic.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 4, 3 | "id": "5b448dbc-5c72-4d83-8038-938b1f8dff19", 4 | "kind": "mnemonic", 5 | "crypto": { 6 | "ciphertext": "6d70fbdceba874f56f15af4b1d060223799288cfc5d276d9ebb91732f5a38c3c59f83896fa7e7eb6a04c05475a6fe4d154de9b9441864c507abd0eb6987dac521b64c0c82783a3cd1e09270cd6cb5ae493f9af694b891253ac1f1ffded68b5ef39c972307e3c33a8354337540908acc795d4df72298dda1ca28ac920983e6a39a01e2bc988bd0b21f864c6de8b5356d11e4b77bc6f75ef", 7 | "cipherparams": { 8 | "iv": "2da5620906634972d9a623bc249d63d4" 9 | }, 10 | "cipher": "aes-128-ctr", 11 | "kdf": "scrypt", 12 | "kdfparams": { 13 | "dklen": 32, 14 | "salt": "aa9e0ba6b188703071a582c10e5331f2756279feb0e2768f1ba0fd38ec77f035", 15 | "n": 4096, 16 | "r": 8, 17 | "p": 1 18 | }, 19 | "mac": "5bc1b20b6d903b8ef3273eedf028112d65eaf85a5ef4215917c1209ec2df715a" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/transactions.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | from typing import Optional, Protocol, TextIO, Union 4 | 5 | from multiversx_sdk import ( 6 | Address, 7 | AwaitingOptions, 8 | TokenTransfer, 9 | Transaction, 10 | TransactionOnNetwork, 11 | TransactionsFactoryConfig, 12 | TransferTransactionsFactory, 13 | ) 14 | 15 | from multiversx_sdk_cli import errors 16 | from multiversx_sdk_cli.base_transactions_controller import BaseTransactionsController 17 | from multiversx_sdk_cli.constants import MIN_GAS_LIMIT 18 | from multiversx_sdk_cli.guardian_relayer_data import GuardianRelayerData 19 | from multiversx_sdk_cli.interfaces import IAccount 20 | 21 | logger = logging.getLogger("transactions") 22 | 23 | 24 | ONE_SECOND_IN_MILLISECONDS = 1000 25 | 26 | 27 | # fmt: off 28 | class INetworkProvider(Protocol): 29 | def send_transaction(self, transaction: Transaction) -> bytes: 30 | ... 31 | 32 | def get_transaction(self, transaction_hash: Union[bytes, str]) -> TransactionOnNetwork: 33 | ... 34 | 35 | def await_transaction_completed(self, transaction_hash: Union[bytes, str], options: Optional[AwaitingOptions] = None) -> TransactionOnNetwork: 36 | ... 37 | # fmt: on 38 | 39 | 40 | class TransactionsController(BaseTransactionsController): 41 | def __init__(self, chain_id: str) -> None: 42 | config = TransactionsFactoryConfig(chain_id) 43 | self.factory = TransferTransactionsFactory(config) 44 | 45 | def create_transaction( 46 | self, 47 | sender: IAccount, 48 | receiver: Address, 49 | native_amount: int, 50 | gas_limit: int, 51 | gas_price: int, 52 | nonce: int, 53 | version: int, 54 | options: int, 55 | guardian_and_relayer_data: GuardianRelayerData, 56 | token_transfers: Optional[list[TokenTransfer]] = None, 57 | data: Optional[str] = None, 58 | ) -> Transaction: 59 | # if no value, token transfers or data provided, create plain transaction 60 | if not native_amount and not token_transfers and not data: 61 | transaction = Transaction( 62 | sender=sender.address, 63 | receiver=receiver, 64 | gas_limit=MIN_GAS_LIMIT, 65 | chain_id=self.factory.config.chain_id, 66 | ) 67 | else: 68 | transaction = self.factory.create_transaction_for_transfer( 69 | sender=sender.address, 70 | receiver=receiver, 71 | native_amount=native_amount, 72 | token_transfers=token_transfers, 73 | data=data.encode() if data else None, 74 | ) 75 | 76 | transaction.gas_price = gas_price 77 | transaction.nonce = nonce 78 | transaction.version = version 79 | transaction.options = options 80 | transaction.guardian = guardian_and_relayer_data.guardian_address 81 | transaction.relayer = guardian_and_relayer_data.relayer_address 82 | 83 | self.add_extra_gas_limit_if_required(transaction) 84 | 85 | if gas_limit: 86 | transaction.gas_limit = gas_limit 87 | 88 | self.sign_transaction( 89 | transaction=transaction, 90 | sender=sender, 91 | guardian=guardian_and_relayer_data.guardian, 92 | relayer=guardian_and_relayer_data.relayer, 93 | guardian_service_url=guardian_and_relayer_data.guardian_service_url, 94 | guardian_2fa_code=guardian_and_relayer_data.guardian_2fa_code, 95 | ) 96 | 97 | return transaction 98 | 99 | 100 | def send_and_wait_for_result(transaction: Transaction, proxy: INetworkProvider, timeout: int) -> TransactionOnNetwork: 101 | if not transaction.signature: 102 | raise errors.TransactionIsNotSigned() 103 | 104 | options = AwaitingOptions(timeout_in_milliseconds=timeout * ONE_SECOND_IN_MILLISECONDS) 105 | 106 | tx_hash = proxy.send_transaction(transaction) 107 | tx_on_network = proxy.await_transaction_completed(tx_hash, options) 108 | 109 | return tx_on_network 110 | 111 | 112 | def load_transaction_from_file(f: TextIO) -> Transaction: 113 | data_json: bytes = f.read().encode() 114 | transaction_dictionary = json.loads(data_json).get("tx") or json.loads(data_json).get("emittedTransaction") 115 | return Transaction.new_from_dictionary(transaction_dictionary) 116 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import os 4 | import pathlib 5 | import shutil 6 | import sys 7 | import tarfile 8 | import zipfile 9 | from pathlib import Path 10 | from types import SimpleNamespace 11 | from typing import Any, Optional, Protocol, Union, runtime_checkable 12 | 13 | import toml 14 | 15 | logger = logging.getLogger("utils") 16 | 17 | 18 | @runtime_checkable 19 | class ISerializable(Protocol): 20 | def to_dictionary(self) -> dict[str, Any]: 21 | return self.__dict__ 22 | 23 | 24 | class Object(ISerializable): 25 | def __repr__(self): 26 | return str(self.__dict__) 27 | 28 | def to_dictionary(self): 29 | return dict(self.__dict__) 30 | 31 | def to_json(self): 32 | data_json = json.dumps(self.__dict__, indent=4) 33 | return data_json 34 | 35 | 36 | class BasicEncoder(json.JSONEncoder): 37 | def default(self, o: Any) -> Any: 38 | if isinstance(o, ISerializable): 39 | return o.to_dictionary() 40 | if isinstance(o, bytes): 41 | return o.hex() 42 | # needed because sdk-py returns SimpleNamespace objects that the json library does not know to serialize 43 | if isinstance(o, SimpleNamespace): 44 | return o.__dict__ 45 | return super().default(o) 46 | 47 | 48 | def omit_fields(data: dict[str, Any], fields: list[str] = []) -> None: 49 | for field in fields: 50 | data.pop(field, None) 51 | 52 | 53 | def untar(archive_path: Path, destination_folder: Path) -> None: 54 | logger.debug(f"untar [{archive_path}] to [{destination_folder}].") 55 | 56 | ensure_folder(destination_folder) 57 | tar = tarfile.open(str(archive_path)) 58 | tar.extractall(path=str(destination_folder)) 59 | tar.close() 60 | 61 | logger.debug("untar done.") 62 | 63 | 64 | def unzip(archive_path: Path, destination_folder: Path): 65 | logger.debug(f"unzip [{archive_path}] to [{destination_folder}].") 66 | 67 | ensure_folder(destination_folder) 68 | with zipfile.ZipFile(str(archive_path), "r") as my_zip: 69 | my_zip.extractall(str(destination_folder)) 70 | 71 | logger.debug("unzip done.") 72 | 73 | 74 | def ensure_folder(folder: Union[str, Path]): 75 | pathlib.Path(folder).mkdir(parents=True, exist_ok=True) 76 | 77 | 78 | def read_lines(file: Path) -> list[str]: 79 | with open(file) as f: 80 | lines = f.readlines() 81 | lines = [line.strip() for line in lines] 82 | lines = [line for line in lines if line] 83 | return lines 84 | 85 | 86 | def write_file(file_path: Path, text: str): 87 | with open(file_path, "w") as file: 88 | return file.write(text) 89 | 90 | 91 | def read_toml_file(filename: Union[str, Path]): 92 | return toml.load(str(filename)) 93 | 94 | 95 | def write_toml_file(filename: Union[str, Path], data: Any): 96 | with open(str(filename), "w") as f: 97 | toml.dump(data, f) 98 | 99 | 100 | def read_json_file(filename: Union[str, Path]) -> Any: 101 | with open(filename) as f: 102 | return json.load(f) 103 | 104 | 105 | def write_json_file(filename: Union[str, Path], data: Any): 106 | with open(str(filename), "w") as f: 107 | json.dump(data, f, indent=4) 108 | 109 | 110 | def dump_out_json(data: Any, outfile: Any = None): 111 | if not outfile: 112 | outfile = sys.stdout 113 | 114 | json.dump(data, outfile, indent=4, cls=BasicEncoder) 115 | outfile.write("\n") 116 | 117 | 118 | def get_subfolders(folder: Path) -> list[str]: 119 | return [item.name for item in os.scandir(folder) if item.is_dir() and not item.name.startswith(".")] 120 | 121 | 122 | def list_files(folder: Path, suffix: Optional[str] = None) -> list[Path]: 123 | folder = folder.expanduser() 124 | files: list[Path] = [folder / file for file in os.listdir(folder)] 125 | files = [file for file in files if file.is_file()] 126 | 127 | if suffix: 128 | files = [file for file in files if str(file).lower().endswith(suffix.lower())] 129 | 130 | return files 131 | 132 | 133 | def remove_folder(folder: Union[str, Path]): 134 | shutil.rmtree(folder, ignore_errors=True) 135 | 136 | 137 | def symlink(real: str, link: str) -> None: 138 | if os.path.islink(link): 139 | os.remove(link) 140 | os.symlink(real, link) 141 | 142 | 143 | def is_arg_present(args: list[str], key: str) -> bool: 144 | for arg in args: 145 | if arg.find("--data") != -1: 146 | continue 147 | if arg.find(key) != -1: 148 | return True 149 | 150 | return False 151 | 152 | 153 | def log_explorer(chain: str, name: str, path: str, details: str): 154 | networks = { 155 | "1": ("MultiversX Mainnet Explorer", "https://explorer.multiversx.com"), 156 | "T": ("MultiversX Testnet Explorer", "https://testnet-explorer.multiversx.com"), 157 | "D": ("MultiversX Devnet Explorer", "https://devnet-explorer.multiversx.com"), 158 | } 159 | try: 160 | explorer_name, explorer_url = networks[chain] 161 | logger.info(f"View this {name} in the {explorer_name}: {explorer_url}/{path}/{details}") 162 | except KeyError: 163 | return 164 | 165 | 166 | def log_explorer_contract_address(chain: str, address: str): 167 | log_explorer(chain, "contract address", "accounts", address) 168 | 169 | 170 | def log_explorer_transaction(chain: str, transaction_hash: str): 171 | log_explorer(chain, "transaction", "transactions", transaction_hash) 172 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/ux.py: -------------------------------------------------------------------------------- 1 | from rich import print 2 | from rich.markup import escape 3 | from rich.panel import Panel 4 | 5 | 6 | def show_message(message: str): 7 | print(Panel(f"[green]{escape(message)}")) 8 | 9 | 10 | def show_critical_error(message: str): 11 | print(Panel(f"[red]{escape(message)}")) 12 | 13 | 14 | def show_warning(message: str): 15 | print(Panel(f"[yellow]{escape(message)}")) 16 | 17 | 18 | def confirm_continuation(message: str): 19 | answer = input(message) 20 | if answer.lower().strip() not in ["y", "yes"]: 21 | print("Confirmation not given. Stopping...") 22 | exit(1) 23 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/version.py: -------------------------------------------------------------------------------- 1 | import importlib.metadata 2 | import logging 3 | from pathlib import Path 4 | 5 | import toml 6 | 7 | 8 | def get_version() -> str: 9 | try: 10 | # Works for local development 11 | return _get_version_from_pyproject() 12 | except Exception: 13 | try: 14 | # Works for the installed package 15 | return _get_version_from_metadata() 16 | except Exception as error: 17 | logging.exception(f"Failed to get version: {error}") 18 | return "unknown" 19 | 20 | 21 | def _get_version_from_pyproject() -> str: 22 | pyproject_path = Path(__file__).parent.parent / "pyproject.toml" 23 | version: str = toml.load(pyproject_path)["project"]["version"] 24 | return version 25 | 26 | 27 | def _get_version_from_metadata() -> str: 28 | try: 29 | return importlib.metadata.version("multiversx_sdk_cli") 30 | except: 31 | return importlib.metadata.version("mxpy") 32 | -------------------------------------------------------------------------------- /multiversx_sdk_cli/workstation.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | from multiversx_sdk_cli import config, utils 5 | 6 | 7 | def get_tools_folder() -> Path: 8 | folder = config.SDK_PATH 9 | utils.ensure_folder(folder) 10 | return folder 11 | 12 | 13 | def is_linux(): 14 | return get_platform() == "linux" 15 | 16 | 17 | def is_windows(): 18 | return get_platform() == "windows" 19 | 20 | 21 | def is_osx(): 22 | return get_platform() == "osx" 23 | 24 | 25 | def get_platform(): 26 | platforms = { 27 | "linux": "linux", 28 | "linux1": "linux", 29 | "linux2": "linux", 30 | "darwin": "osx", 31 | "win32": "windows", 32 | "cygwin": "windows", 33 | "msys": "windows", 34 | } 35 | 36 | platform = platforms.get(sys.platform) 37 | if platform is None: 38 | raise Exception(f"Unknown platform: {sys.platform}") 39 | 40 | return platform 41 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | python_version = 3.9 3 | warn_return_any = True 4 | warn_unused_configs = True 5 | 6 | [mypy-debugpy.*] 7 | ignore_missing_imports = True 8 | 9 | [mypy-ledgercomm.*] 10 | ignore_missing_imports = True 11 | 12 | [mypy-semver.*] 13 | ignore_missing_imports = True 14 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "multiversx-sdk-cli" 7 | version = "10.2.0" 8 | authors = [ 9 | { name="MultiversX" }, 10 | ] 11 | license = "MIT" 12 | description = "MultiversX Smart Contracts Tools" 13 | readme = "README.md" 14 | requires-python = ">=3.8" 15 | classifiers = [ 16 | "Programming Language :: Python :: 3", 17 | "License :: OSI Approved :: MIT License", 18 | "Operating System :: OS Independent", 19 | "Intended Audience :: Developers" 20 | ] 21 | 22 | dependencies = [ 23 | "toml>=0.10.2", 24 | "requests>=2.32.0,<3.0.0", 25 | "ledgercomm[hid]", 26 | "rich==13.3.4", 27 | "argcomplete==3.2.2", 28 | "multiversx-sdk[ledger]==1.2.0" 29 | ] 30 | 31 | [project.scripts] 32 | mxpy = "multiversx_sdk_cli.cli:main" 33 | 34 | [tool.hatch.build] 35 | include = [ 36 | "multiversx_sdk_cli/**" 37 | ] 38 | exclude = [ 39 | ".github", 40 | "multiversx_sdk_cli/tests/**" 41 | ] 42 | 43 | [project.urls] 44 | "Homepage" = "https://github.com/multiversx/mx-sdk-py-cli" 45 | -------------------------------------------------------------------------------- /pyrightconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "multiversx_sdk_cli" 4 | ], 5 | "exclude": [ 6 | "**/__pycache__" 7 | ], 8 | "ignore": [], 9 | "defineConstant": { 10 | "DEBUG": true 11 | }, 12 | "venvPath": ".", 13 | "venv": "venv", 14 | "stubPath": "", 15 | "reportMissingImports": true, 16 | "reportMissingTypeStubs": false, 17 | "reportUnknownParameterType": true 18 | } 19 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | skip_on_windows: marks tests as being skipped when running on windows (deselect with '-m "skip_on_windows"') 4 | only: only run a specific test (run using: pytest -m "only") 5 | require_localnet: marks tests that require a localnet (select with '-m "require_localnet"') 6 | 7 | log_cli = True 8 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | flake8 3 | autopep8 4 | pytest-mock 5 | pre-commit 6 | black 7 | mypy 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | toml>=0.10.2 2 | types-toml 3 | requests>=2.32.0,<3.0.0 4 | types-requests 5 | ledgercomm[hid] 6 | rich==13.3.4 7 | argcomplete==3.2.2 8 | 9 | multiversx-sdk[ledger]==1.2.0 10 | --------------------------------------------------------------------------------