diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml index e800b693..e7010de2 100644 --- a/.github/workflows/build-windows.yml +++ b/.github/workflows/build-windows.yml @@ -30,7 +30,7 @@ jobs: run: | python3 -m pip install --upgrade pip pip3 install -r requirements.txt - pip3 install pytest + pip3 install -r requirements-dev.txt - name: Set github_api_token shell: bash run: | diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 012b4f9f..9dda9a37 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,7 +29,7 @@ jobs: run: | python3 -m pip install --upgrade pip pip3 install -r requirements.txt - pip3 install pytest + pip3 install -r requirements-dev.txt - name: Set github_api_token run: | mkdir ~/multiversx-sdk diff --git a/CLI.md b/CLI.md index e3721cbb..3cf655de 100644 --- a/CLI.md +++ b/CLI.md @@ -23,7 +23,7 @@ See: COMMAND GROUPS: - {contract,tx,validator,account,ledger,wallet,deps,config,localnet,data,staking-provider,dns} + {contract,tx,validator,account,ledger,wallet,deps,config,localnet,data,staking-provider,dns,faucet} TOP-LEVEL OPTIONS: -h, --help show this help message and exit @@ -45,6 +45,7 @@ localnet Set up, start and control localnets data Data manipulation omnitool staking-provider Staking provider omnitool dns Operations related to the Domain Name Service +faucet Get xEGLD on Devnet or Testnet ``` ## Group **Contract** @@ -69,7 +70,7 @@ new Create a new Smart Contract project based on a te templates List the available Smart Contract templates. build Build a Smart Contract project. clean Clean a Smart Contract project. -test Run scenarios (tests). +test Run tests. report Print a detailed report of the smart contracts. deploy Deploy a Smart Contract. call Interact with a Smart Contract (execute function). @@ -194,6 +195,7 @@ Output example: options: -h, --help show this help message and exit --bytecode BYTECODE the file containing the WASM bytecode + --abi ABI the ABI of the Smart Contract --metadata-not-upgradeable ‼ mark the contract as NOT upgradeable (default: upgradeable) --metadata-not-readable ‼ mark the contract as NOT readable (default: readable) --metadata-payable ‼ mark the contract as payable (default: not payable) @@ -219,10 +221,18 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --arguments ARGUMENTS [ARGUMENTS ...] arguments for the contract transaction, as [number, bech32-address, ascii string, boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true erd1[..] + --arguments-file ARGUMENTS_FILE a json file containing the arguments. ONLY if abi file is provided. + E.g. [{ 'to': 'erd1...', 'amount': 10000000000 }] --wait-result signal to wait for the transaction result - only valid if --send is set --timeout TIMEOUT max num of seconds to wait for result - only valid if --wait-result is @@ -282,6 +292,7 @@ positional arguments: options: -h, --help show this help message and exit + --abi ABI the ABI of the Smart Contract --outfile OUTFILE where to save the output (default: stdout) --pem PEM 🔑 the PEM file, if keyfile not provided --pem-index PEM_INDEX 🔑 the index in the PEM file (default: 0) @@ -303,11 +314,19 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --function FUNCTION the function to call --arguments ARGUMENTS [ARGUMENTS ...] arguments for the contract transaction, as [number, bech32-address, ascii string, boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true erd1[..] + --arguments-file ARGUMENTS_FILE a json file containing the arguments. ONLY if abi file is provided. + E.g. [{ 'to': 'erd1...', 'amount': 10000000000 }] --token-transfers TOKEN_TRANSFERS [TOKEN_TRANSFERS ...] token transfers for transfer & execute, as [token, amount] E.g. --token-transfers NFT-123456-0a 1 ESDT-987654 100000000 @@ -371,6 +390,7 @@ positional arguments: options: -h, --help show this help message and exit + --abi ABI the ABI of the Smart Contract --outfile OUTFILE where to save the output (default: stdout) --bytecode BYTECODE the file containing the WASM bytecode --metadata-not-upgradeable ‼ mark the contract as NOT upgradeable (default: upgradeable) @@ -397,10 +417,18 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --arguments ARGUMENTS [ARGUMENTS ...] arguments for the contract transaction, as [number, bech32-address, ascii string, boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true erd1[..] + --arguments-file ARGUMENTS_FILE a json file containing the arguments. ONLY if abi file is provided. + E.g. [{ 'to': 'erd1...', 'amount': 10000000000 }] --wait-result signal to wait for the transaction result - only valid if --send is set --timeout TIMEOUT max num of seconds to wait for result - only valid if --wait-result is @@ -432,11 +460,14 @@ positional arguments: options: -h, --help show this help message and exit + --abi ABI the ABI of the Smart Contract --proxy PROXY 🔗 the URL of the proxy --function FUNCTION the function to call --arguments ARGUMENTS [ARGUMENTS ...] arguments for the contract transaction, as [number, bech32-address, ascii string, boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true erd1[..] + --arguments-file ARGUMENTS_FILE a json file containing the arguments. ONLY if abi file is provided. E.g. [{ + 'to': 'erd1...', 'amount': 10000000000 }] ``` ### Contract.Report @@ -549,8 +580,17 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --data-file DATA_FILE a file containing transaction data + --token-transfers TOKEN_TRANSFERS [TOKEN_TRANSFERS ...] + token transfers for transfer & execute, as [token, amount] E.g. + --token-transfers NFT-123456-0a 1 ESDT-987654 100000000 --outfile OUTFILE where to save the output (signed transaction, hash) (default: stdout) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -693,6 +733,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -743,6 +789,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -791,6 +843,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -839,6 +897,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -887,6 +951,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -935,6 +1005,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -960,11 +1036,34 @@ usage: mxpy staking-provider COMMAND [-h] ... Staking provider omnitool COMMANDS: - {create-new-delegation-contract,get-contract-address,add-nodes,remove-nodes,stake-nodes,unbond-nodes,unstake-nodes,unjail-nodes,change-service-fee,modify-delegation-cap,automatic-activation,redelegate-cap,set-metadata} + {create-new-delegation-contract,get-contract-address,add-nodes,remove-nodes,stake-nodes,unbond-nodes,unstake-nodes,unjail-nodes,delegate,claim-rewards,redelegate-rewards,undelegate,withdraw,change-service-fee,modify-delegation-cap,automatic-activation,redelegate-cap,set-metadata,make-delegation-contract-from-validator} OPTIONS: -h, --help show this help message and exit +---------------- +COMMANDS summary +---------------- +create-new-delegation-contract Create a new delegation system smart contract, transferred value must be greater than baseIssuingCost + min deposit value +get-contract-address Get create contract address by transaction hash +add-nodes Add new nodes must be called by the contract owner +remove-nodes Remove nodes must be called by the contract owner +stake-nodes Stake nodes must be called by the contract owner +unbond-nodes Unbond nodes must be called by the contract owner +unstake-nodes Unstake nodes must be called by the contract owner +unjail-nodes Unjail nodes must be called by the contract owner +delegate Delegate funds to a delegation contract +claim-rewards Claim the rewards earned for delegating +redelegate-rewards Redelegate the rewards earned for delegating +undelegate Undelegate funds from a delegation contract +withdraw Withdraw funds from a delegation contract +change-service-fee Change service fee must be called by the contract owner +modify-delegation-cap Modify delegation cap must be called by the contract owner +automatic-activation Automatic activation must be called by the contract owner +redelegate-cap Redelegate cap must be called by the contract owner +set-metadata Set metadata must be called by the contract owner +make-delegation-contract-from-validator Create a delegation contract from validator data. Must be called by the node operator + ``` ### StakingProvider.CreateNewDelegationContract @@ -973,7 +1072,7 @@ OPTIONS: $ mxpy staking-provider create-new-delegation-contract --help usage: mxpy staking-provider create-new-delegation-contract [-h] ... -Create a new delegation system smart contract, transferred value must begreater than baseIssuingCost + min deposit value +Create a new delegation system smart contract, transferred value must be greater than baseIssuingCost + min deposit value options: -h, --help show this help message and exit @@ -998,6 +1097,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1044,7 +1149,6 @@ options: -h, --help show this help message and exit --validators-file VALIDATORS_FILE a JSON file describing the Nodes --delegation-contract DELEGATION_CONTRACT address of the delegation contract - --using-delegation-manager whether delegation contract was created using the Delegation Manager --proxy PROXY 🔗 the URL of the proxy --pem PEM 🔑 the PEM file, if keyfile not provided --pem-index PEM_INDEX 🔑 the index in the PEM file (default: 0) @@ -1066,6 +1170,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1093,6 +1203,7 @@ Remove nodes must be called by the contract owner options: -h, --help show this help message and exit --bls-keys BLS_KEYS a list with the bls keys of the nodes + --validators-file VALIDATORS_FILE a JSON file describing the Nodes --delegation-contract DELEGATION_CONTRACT address of the delegation contract --proxy PROXY 🔗 the URL of the proxy --pem PEM 🔑 the PEM file, if keyfile not provided @@ -1115,6 +1226,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1142,6 +1259,7 @@ Stake nodes must be called by the contract owner options: -h, --help show this help message and exit --bls-keys BLS_KEYS a list with the bls keys of the nodes + --validators-file VALIDATORS_FILE a JSON file describing the Nodes --delegation-contract DELEGATION_CONTRACT address of the delegation contract --proxy PROXY 🔗 the URL of the proxy --pem PEM 🔑 the PEM file, if keyfile not provided @@ -1164,6 +1282,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1191,6 +1315,7 @@ Unbond nodes must be called by the contract owner options: -h, --help show this help message and exit --bls-keys BLS_KEYS a list with the bls keys of the nodes + --validators-file VALIDATORS_FILE a JSON file describing the Nodes --delegation-contract DELEGATION_CONTRACT address of the delegation contract --proxy PROXY 🔗 the URL of the proxy --pem PEM 🔑 the PEM file, if keyfile not provided @@ -1213,6 +1338,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1240,6 +1371,7 @@ Unstake nodes must be called by the contract owner options: -h, --help show this help message and exit --bls-keys BLS_KEYS a list with the bls keys of the nodes + --validators-file VALIDATORS_FILE a JSON file describing the Nodes --delegation-contract DELEGATION_CONTRACT address of the delegation contract --proxy PROXY 🔗 the URL of the proxy --pem PEM 🔑 the PEM file, if keyfile not provided @@ -1262,6 +1394,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1289,6 +1427,7 @@ Unjail nodes must be called by the contract owner options: -h, --help show this help message and exit --bls-keys BLS_KEYS a list with the bls keys of the nodes + --validators-file VALIDATORS_FILE a JSON file describing the Nodes --delegation-contract DELEGATION_CONTRACT address of the delegation contract --proxy PROXY 🔗 the URL of the proxy --pem PEM 🔑 the PEM file, if keyfile not provided @@ -1311,6 +1450,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1360,6 +1505,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1409,6 +1560,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1459,6 +1616,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1509,6 +1672,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1560,6 +1729,12 @@ options: --guardian GUARDIAN the address of the guradian --guardian-service-url GUARDIAN_SERVICE_URL the url of the guardian service --guardian-2fa-code GUARDIAN_2FA_CODE the 2fa code for the guardian + --relayer RELAYER the address of the relayer + --inner-transactions INNER_TRANSACTIONS a json file containing the inner transactions; should only be provided + when creating the relayer's transaction + --inner-transactions-outfile INNER_TRANSACTIONS_OUTFILE + where to save the transaction as an inner transaction (default: + stdout) --options OPTIONS the transaction options (default: 0) --send ✓ whether to broadcast the transaction (default: False) --simulate whether to simulate the transaction (default: False) @@ -1657,6 +1832,7 @@ options: (default: None) --address-hrp ADDRESS_HRP the human-readable part of the address, when format is keystore- secret-key or pem (default: erd) + --shard SHARD the shard in which the address will be generated; (default: random) ``` ### Wallet.Convert @@ -1888,11 +2064,11 @@ usage: mxpy deps install [-h] ... Install dependencies or multiversx-sdk modules. positional arguments: - {all,rust,golang,vmtools,testwallets} the dependency to install + {all,rust,golang,testwallets} the dependency to install options: - -h, --help show this help message and exit - --overwrite whether to overwrite an existing installation + -h, --help show this help message and exit + --overwrite whether to overwrite an existing installation ``` ### Dependencies.Check @@ -1905,10 +2081,10 @@ usage: mxpy deps check [-h] ... Check whether a dependency is installed. positional arguments: - {all,rust,golang,vmtools,testwallets} the dependency to check + {all,rust,golang,testwallets} the dependency to check options: - -h, --help show this help message and exit + -h, --help show this help message and exit ``` ## Group **Configuration** @@ -2116,3 +2292,46 @@ options: --use-global use the global storage (default: False) ``` +## Group **Faucet** + + +``` +$ mxpy faucet --help +usage: mxpy faucet COMMAND [-h] ... + +Get xEGLD on Devnet or Testnet + +COMMANDS: + {request} + +OPTIONS: + -h, --help show this help message and exit + +---------------- +COMMANDS summary +---------------- +request Request xEGLD. + +``` +### Faucet.Request + + +``` +$ mxpy faucet request --help +usage: mxpy faucet request [-h] ... + +Request xEGLD. + +options: + -h, --help show this help message and exit + --pem PEM 🔑 the PEM file, if keyfile not provided + --pem-index PEM_INDEX 🔑 the index in the PEM file (default: 0) + --keyfile KEYFILE 🔑 a JSON keyfile, if PEM not provided + --passfile PASSFILE 🔑 a file containing keyfile's password, if keyfile provided + --ledger 🔐 bool flag for signing transaction using ledger + --ledger-account-index LEDGER_ACCOUNT_INDEX 🔐 the index of the account when using Ledger + --ledger-address-index LEDGER_ADDRESS_INDEX 🔐 the index of the address when using Ledger + --sender-username SENDER_USERNAME 🖄 the username of the sender + --chain CHAIN the chain identifier + +``` diff --git a/CLI.md.sh b/CLI.md.sh index 49790861..8bb90601 100755 --- a/CLI.md.sh +++ b/CLI.md.sh @@ -114,6 +114,9 @@ generate() { command "Data.Dump" "data parse" command "Data.Store" "data store" command "Data.Load" "data load" + + group "Faucet" "faucet" + command "Faucet.Request" "faucet request" } generate diff --git a/multiversx_sdk_cli/accounts.py b/multiversx_sdk_cli/accounts.py index 515093b1..489f03a7 100644 --- a/multiversx_sdk_cli/accounts.py +++ b/multiversx_sdk_cli/accounts.py @@ -2,10 +2,9 @@ from pathlib import Path from typing import Any, Optional, Protocol -from multiversx_sdk_core import (Address, Message, MessageComputer, - TransactionComputer) -from multiversx_sdk_network_providers.accounts import AccountOnNetwork -from multiversx_sdk_wallet import UserSigner +from multiversx_sdk import (Address, Message, MessageComputer, + TransactionComputer, UserSigner) +from multiversx_sdk.network_providers.accounts import AccountOnNetwork from multiversx_sdk_cli.constants import DEFAULT_HRP from multiversx_sdk_cli.interfaces import IAccount, IAddress, ITransaction diff --git a/multiversx_sdk_cli/cli.py b/multiversx_sdk_cli/cli.py index 91ca09d0..79eacf20 100644 --- a/multiversx_sdk_cli/cli.py +++ b/multiversx_sdk_cli/cli.py @@ -1,9 +1,11 @@ +# PYTHON_ARGCOMPLETE_OK import argparse import logging import sys from argparse import ArgumentParser from typing import Any, List +import argcomplete from rich.logging import RichHandler import multiversx_sdk_cli.cli_accounts @@ -13,6 +15,7 @@ import multiversx_sdk_cli.cli_delegation import multiversx_sdk_cli.cli_deps import multiversx_sdk_cli.cli_dns +import multiversx_sdk_cli.cli_faucet import multiversx_sdk_cli.cli_ledger import multiversx_sdk_cli.cli_localnet import multiversx_sdk_cli.cli_transactions @@ -41,6 +44,7 @@ def _do_main(cli_args: List[str]): utils.ensure_folder(config.SDK_PATH) argv_with_config_args = config.add_config_args(cli_args) parser = setup_parser(argv_with_config_args) + argcomplete.autocomplete(parser) args = parser.parse_args(argv_with_config_args) if args.verbose: @@ -97,6 +101,7 @@ def setup_parser(args: List[str]): commands.append(multiversx_sdk_cli.cli_data.setup_parser(subparsers)) commands.append(multiversx_sdk_cli.cli_delegation.setup_parser(args, subparsers)) commands.append(multiversx_sdk_cli.cli_dns.setup_parser(args, subparsers)) + commands.append(multiversx_sdk_cli.cli_faucet.setup_parser(args, subparsers)) parser.epilog = """ ---------------------- diff --git a/multiversx_sdk_cli/cli_accounts.py b/multiversx_sdk_cli/cli_accounts.py index 1ebf8bc6..9ae5933a 100644 --- a/multiversx_sdk_cli/cli_accounts.py +++ b/multiversx_sdk_cli/cli_accounts.py @@ -1,9 +1,7 @@ import logging from typing import Any -from multiversx_sdk_core import Address -from multiversx_sdk_network_providers.proxy_network_provider import \ - ProxyNetworkProvider +from multiversx_sdk import Address, ProxyNetworkProvider from multiversx_sdk_cli import cli_shared, utils diff --git a/multiversx_sdk_cli/cli_config.py b/multiversx_sdk_cli/cli_config.py index d1439ac2..4e03879d 100644 --- a/multiversx_sdk_cli/cli_config.py +++ b/multiversx_sdk_cli/cli_config.py @@ -103,7 +103,7 @@ def list_configs(args: Any): def delete_config(args: Any): config_file = config.resolve_config_path() if not config_file.is_file(): - logger.info(f"Config file not found. Aborting...") + logger.info("Config file not found. Aborting...") return confirm_continuation(f"The file `{str(config_file)}` will be deleted. Do you want to continue? (y/n)") diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py index bb71299f..8f7c14a9 100644 --- a/multiversx_sdk_cli/cli_contracts.py +++ b/multiversx_sdk_cli/cli_contracts.py @@ -1,19 +1,19 @@ +import json import logging import os from pathlib import Path -from typing import Any, List +from typing import Any, List, Tuple -from multiversx_sdk_core import Address, AddressComputer, Transaction -from multiversx_sdk_core.transaction_factories import TransactionsFactoryConfig -from multiversx_sdk_network_providers.proxy_network_provider import \ - ProxyNetworkProvider +from multiversx_sdk import (Address, AddressComputer, ProxyNetworkProvider, + Transaction, TransactionsFactoryConfig) +from multiversx_sdk.abi import Abi from multiversx_sdk_cli import cli_shared, projects, utils from multiversx_sdk_cli.cli_output import CLIOutputBuilder from multiversx_sdk_cli.constants import NUMBER_OF_SHARDS from multiversx_sdk_cli.contract_verification import \ trigger_contract_verification -from multiversx_sdk_cli.contracts import SmartContract, query_contract +from multiversx_sdk_cli.contracts import SmartContract from multiversx_sdk_cli.cosign_transaction import cosign_transaction from multiversx_sdk_cli.dependency_checker import check_if_rust_is_installed from multiversx_sdk_cli.docker import is_docker_installed, run_docker @@ -21,7 +21,7 @@ from multiversx_sdk_cli.interfaces import IAddress from multiversx_sdk_cli.projects.core import get_project_paths_recursively from multiversx_sdk_cli.projects.templates import Contract -from multiversx_sdk_cli.ux import show_message +from multiversx_sdk_cli.ux import show_message, show_warning logger = logging.getLogger("cli.contracts") @@ -53,12 +53,13 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.add_argument("--path", default=os.getcwd(), help="the project directory (default: current directory)") sub.set_defaults(func=clean) - sub = cli_shared.add_command_subparser(subparsers, "contract", "test", "Run scenarios (tests).") - _add_project_arg(sub) - sub.add_argument("--directory", default="scenarios", - help="🗀 the directory containing the tests (default: %(default)s)") - sub.add_argument("--wildcard", required=False, help="wildcard to match only specific test files") - _add_recursive_arg(sub) + sub = cli_shared.add_command_subparser(subparsers, "contract", "test", "Run tests.") + sub.add_argument("--path", default=os.getcwd(), + help="the directory of the contract (default: %(default)s)") + sub.add_argument("--go", action="store_true", + help="this arg runs rust and go tests (default: false)") + sub.add_argument("--scen", action="store_true", help="this arg runs scenarios (default: false). If `--scen` and `--go` are both specified, scen overrides the go argument") + sub.add_argument("--nocapture", action="store_true", help="this arg prints the entire output of the vm (default: false)") sub.set_defaults(func=run_tests) sub = cli_shared.add_command_subparser(subparsers, "contract", "report", "Print a detailed report of the smart contracts.") @@ -73,11 +74,12 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: output_description = CLIOutputBuilder.describe(with_contract=True, with_transaction_on_network=True, with_simulation=True) sub = cli_shared.add_command_subparser(subparsers, "contract", "deploy", f"Deploy a Smart Contract.{output_description}") _add_bytecode_arg(sub) + _add_contract_abi_arg(sub) _add_metadata_arg(sub) cli_shared.add_outfile_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) _add_arguments_arg(sub) sub.add_argument("--wait-result", action="store_true", default=False, help="signal to wait for the transaction result - only valid if --send is set") @@ -91,10 +93,11 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "contract", "call", f"Interact with a Smart Contract (execute function).{output_description}") _add_contract_arg(sub) + _add_contract_abi_arg(sub) cli_shared.add_outfile_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) _add_function_arg(sub) _add_arguments_arg(sub) _add_token_transfers_args(sub) @@ -110,12 +113,13 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "contract", "upgrade", f"Upgrade a previously-deployed Smart Contract.{output_description}") _add_contract_arg(sub) + _add_contract_abi_arg(sub) cli_shared.add_outfile_arg(sub) _add_bytecode_arg(sub) _add_metadata_arg(sub) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) _add_arguments_arg(sub) sub.add_argument("--wait-result", action="store_true", default=False, help="signal to wait for the transaction result - only valid if --send is set") @@ -129,6 +133,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "contract", "query", "Query a Smart Contract (call a pure function)") _add_contract_arg(sub) + _add_contract_abi_arg(sub) cli_shared.add_proxy_arg(sub) _add_function_arg(sub) _add_arguments_arg(sub) @@ -172,7 +177,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: def _add_project_arg(sub: Any): sub.add_argument("project", nargs='?', default=os.getcwd(), - help="🗀 the project directory (default: current directory)") + help="the project directory (default: current directory)") def _add_build_options_sc_meta(sub: Any): @@ -213,10 +218,6 @@ def _add_build_options_args(sub: Any): help="for rust projects, optionally specify the suffix of the wasm bytecode output file") -def _add_recursive_arg(sub: Any): - sub.add_argument("-r", "--recursive", dest="recursive", action="store_true", help="locate projects recursively") - - def _add_bytecode_arg(sub: Any): sub.add_argument("--bytecode", type=str, required=True, help="the file containing the WASM bytecode") @@ -226,6 +227,10 @@ def _add_contract_arg(sub: Any): sub.add_argument("contract", help="🖄 the address of the Smart Contract") +def _add_contract_abi_arg(sub: Any): + sub.add_argument("--abi", type=str, help="the ABI of the Smart Contract") + + def _add_function_arg(sub: Any): sub.add_argument("--function", required=True, type=str, help="the function to call") @@ -234,6 +239,8 @@ def _add_arguments_arg(sub: Any): sub.add_argument("--arguments", nargs='+', help="arguments for the contract transaction, as [number, bech32-address, ascii string, " "boolean] or hex-encoded. E.g. --arguments 42 0x64 1000 0xabba str:TOK-a1c2ef true erd1[..]") + sub.add_argument("--arguments-file", type=str, help="a json file containing the arguments. ONLY if abi file is provided. " + "E.g. [{ 'to': 'erd1...', 'amount': 10000000000 }]") def _add_token_transfers_args(sub: Any): @@ -286,25 +293,28 @@ def clean(args: Any): def build(args: Any): - check_if_rust_is_installed() project_paths = [Path(args.path)] arg_list = cli_shared.convert_args_object_to_args_list(args) for project in project_paths: projects.build_project(project, arg_list) + show_warning("The primary tool for building smart contracts is `sc-meta`. Try using the `sc-meta all build` command.") + def do_report(args: Any): - check_if_rust_is_installed() + deprecation_message = "`mxpy contract report` is deprecated. Please use `sc-meta report` instead." + logger.warning(deprecation_message) + args_dict = args.__dict__ projects.do_report(args, args_dict) + show_warning(deprecation_message) + def run_tests(args: Any): check_if_rust_is_installed() - project_paths = get_project_paths(args) - for project in project_paths: - projects.run_tests(project, args) + projects.run_tests(args) def deploy(args: Any): @@ -316,15 +326,16 @@ def deploy(args: Any): sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) + abi = Abi.load(Path(args.abi)) if args.abi else None + contract = SmartContract(config, abi) - address_computer = AddressComputer(NUMBER_OF_SHARDS) - contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=args.nonce) + arguments, should_prepare_args = _get_contract_arguments(args) tx = contract.prepare_deploy_transaction( owner=sender, bytecode=Path(args.bytecode), - arguments=args.arguments, + arguments=arguments, + should_prepare_args=should_prepare_args, upgradeable=args.metadata_upgradeable, readable=args.metadata_readable, payable=args.metadata_payable, @@ -337,6 +348,9 @@ def deploy(args: Any): guardian=args.guardian) tx = _sign_guarded_tx(args, tx) + address_computer = AddressComputer(NUMBER_OF_SHARDS) + contract_address = address_computer.compute_contract_address(deployer=sender.address, deployment_nonce=args.nonce) + logger.info("Contract address: %s", contract_address.to_bech32()) utils.log_explorer_contract_address(args.chain, contract_address.to_bech32()) @@ -365,15 +379,20 @@ def call(args: Any): cli_shared.prepare_nonce_in_args(args) sender = cli_shared.prepare_account(args) + config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) + abi = Abi.load(Path(args.abi)) if args.abi else None + contract = SmartContract(config, abi) + + arguments, should_prepare_args = _get_contract_arguments(args) contract_address = Address.new_from_bech32(args.contract) tx = contract.prepare_execute_transaction( caller=sender, contract=contract_address, function=args.function, - arguments=args.arguments, + arguments=arguments, + should_prepare_args=should_prepare_args, gas_limit=int(args.gas_limit), value=int(args.value), transfers=args.token_transfers, @@ -395,14 +414,18 @@ def upgrade(args: Any): sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) - contract = SmartContract(config) + abi = Abi.load(Path(args.abi)) if args.abi else None + contract = SmartContract(config, abi) + + arguments, should_prepare_args = _get_contract_arguments(args) contract_address = Address.new_from_bech32(args.contract) tx = contract.prepare_upgrade_transaction( owner=sender, contract=contract_address, bytecode=Path(args.bytecode), - arguments=args.arguments, + arguments=arguments, + should_prepare_args=should_prepare_args, upgradeable=args.metadata_upgradeable, readable=args.metadata_readable, payable=args.metadata_payable, @@ -421,20 +444,46 @@ def upgrade(args: Any): def query(args: Any): logger.debug("query") - # workaround so we can use the function bellow + # workaround so we can use the function below to set chainID args.chain = "" cli_shared.prepare_chain_id_in_args(args) + config = TransactionsFactoryConfig(args.chain) + abi = Abi.load(Path(args.abi)) if args.abi else None + contract = SmartContract(config, abi) + + arguments, should_prepare_args = _get_contract_arguments(args) contract_address = Address.new_from_bech32(args.contract) proxy = ProxyNetworkProvider(args.proxy) function = args.function - arguments: List[Any] = args.arguments or [] - result = query_contract(contract_address, proxy, function, arguments) + result = contract.query_contract( + contract_address=contract_address, + proxy=proxy, + function=function, + arguments=arguments, + should_prepare_args=should_prepare_args + ) + utils.dump_out_json(result) +def _get_contract_arguments(args: Any) -> Tuple[List[Any], bool]: + json_args = json.loads(Path(args.arguments_file).expanduser().read_text()) if args.arguments_file else None + + if json_args and args.arguments: + raise Exception("Provide either '--arguments' or '--arguments-file'.") + + if json_args: + if not args.abi: + raise Exception("Can't use '--arguments-file' without providing the Abi file.") + + return json_args, False + else: + return args.arguments, True + + def _send_or_simulate(tx: Transaction, contract_address: IAddress, args: Any): output_builder = cli_shared.send_or_simulate(tx, args, dump_output=False) output_builder.set_contract_address(contract_address) diff --git a/multiversx_sdk_cli/cli_delegation.py b/multiversx_sdk_cli/cli_delegation.py index 7d936c18..aafbaa4b 100644 --- a/multiversx_sdk_cli/cli_delegation.py +++ b/multiversx_sdk_cli/cli_delegation.py @@ -1,8 +1,6 @@ from typing import Any, List -from multiversx_sdk_core.transaction_factories import TransactionsFactoryConfig -from multiversx_sdk_network_providers.proxy_network_provider import \ - ProxyNetworkProvider +from multiversx_sdk import ProxyNetworkProvider, TransactionsFactoryConfig from multiversx_sdk_cli import cli_shared, errors, utils from multiversx_sdk_cli.delegation import DelegationOperations @@ -14,7 +12,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: # create new delegation contract sub = cli_shared.add_command_subparser(subparsers, "staking-provider", "create-new-delegation-contract", - "Create a new delegation system smart contract, transferred value must be" + "Create a new delegation system smart contract, transferred value must be " "greater than baseIssuingCost + min deposit value") _add_common_arguments(args, sub) sub.add_argument("--total-delegation-cap", required=True, help="the total delegation contract capacity") @@ -34,7 +32,6 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: "Add new nodes must be called by the contract owner") sub.add_argument("--validators-file", required=True, help="a JSON file describing the Nodes") sub.add_argument("--delegation-contract", required=True, help="address of the delegation contract") - sub.add_argument("--using-delegation-manager", action="store_true", required=False, help="whether delegation contract was created using the Delegation Manager") _add_common_arguments(args, sub) sub.set_defaults(func=add_new_nodes) @@ -83,6 +80,41 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: _add_common_arguments(args, sub) sub.set_defaults(func=unjail_nodes) + # delegate + sub = cli_shared.add_command_subparser(subparsers, "staking-provider", "delegate", + "Delegate funds to a delegation contract") + sub.add_argument("--delegation-contract", required=True, help="address of the delegation contract") + _add_common_arguments(args, sub) + sub.set_defaults(func=delegate) + + # claim rewards + sub = cli_shared.add_command_subparser(subparsers, "staking-provider", "claim-rewards", + "Claim the rewards earned for delegating") + sub.add_argument("--delegation-contract", required=True, help="address of the delegation contract") + _add_common_arguments(args, sub) + sub.set_defaults(func=claim_rewards) + + # redelegate rewards + sub = cli_shared.add_command_subparser(subparsers, "staking-provider", "redelegate-rewards", + "Redelegate the rewards earned for delegating") + sub.add_argument("--delegation-contract", required=True, help="address of the delegation contract") + _add_common_arguments(args, sub) + sub.set_defaults(func=redelegate_rewards) + + # undelegate + sub = cli_shared.add_command_subparser(subparsers, "staking-provider", "undelegate", + "Undelegate funds from a delegation contract") + sub.add_argument("--delegation-contract", required=True, help="address of the delegation contract") + _add_common_arguments(args, sub) + sub.set_defaults(func=undelegate) + + # withdraw + sub = cli_shared.add_command_subparser(subparsers, "staking-provider", "withdraw", + "Withdraw funds from a delegation contract") + sub.add_argument("--delegation-contract", required=True, help="address of the delegation contract") + _add_common_arguments(args, sub) + sub.set_defaults(func=withdraw) + # change service fee sub = cli_shared.add_command_subparser(subparsers, "staking-provider", "change-service-fee", "Change service fee must be called by the contract owner") @@ -134,22 +166,38 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: _add_common_arguments(args, sub) sub.set_defaults(func=set_metadata) + # convert validator to delegation contract + sub = cli_shared.add_command_subparser(subparsers, "staking-provider", "make-delegation-contract-from-validator", + "Create a delegation contract from validator data. Must be called by the node operator") + + sub.add_argument("--max-cap", required=True, help="total delegation cap in EGLD, fully denominated. Use value 0 for uncapped") + sub.add_argument("--fee", required=True, help="service fee as hundredths of percents. (e.g. a service fee of 37.45 percent is expressed by the integer 3745)") + _add_common_arguments(args, sub) + sub.set_defaults(func=make_new_contract_from_validator_data) + + parser.epilog = cli_shared.build_group_epilog(subparsers) + return subparsers + def _add_common_arguments(args: List[str], sub: Any): cli_shared.add_proxy_arg(sub) cli_shared.add_wallet_args(args, sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_estimate_gas=True, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_estimate_gas=True) cli_shared.add_broadcast_args(sub, relay=False) cli_shared.add_outfile_arg(sub, what="signed transaction, hash") cli_shared.add_guardian_wallet_args(args, sub) -def do_create_delegation_contract(args: Any): +def ensure_arguments_are_provided_and_prepared(args: Any): cli_shared.check_guardian_and_options_args(args) cli_shared.check_broadcast_args(args) cli_shared.prepare_chain_id_in_args(args) cli_shared.prepare_nonce_in_args(args) + +def do_create_delegation_contract(args: Any): + ensure_arguments_are_provided_and_prepared(args) + sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) delegation = DelegationOperations(config) @@ -173,10 +221,7 @@ def get_contract_address_by_deploy_tx_hash(args: Any): def add_new_nodes(args: Any): - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -188,10 +233,7 @@ def add_new_nodes(args: Any): def remove_nodes(args: Any): _check_if_either_bls_keys_or_validators_file_are_provided(args) - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -203,10 +245,7 @@ def remove_nodes(args: Any): def stake_nodes(args: Any): _check_if_either_bls_keys_or_validators_file_are_provided(args) - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -226,10 +265,7 @@ def _check_if_either_bls_keys_or_validators_file_are_provided(args: Any): def unbond_nodes(args: Any): _check_if_either_bls_keys_or_validators_file_are_provided(args) - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -241,10 +277,7 @@ def unbond_nodes(args: Any): def unstake_nodes(args: Any): _check_if_either_bls_keys_or_validators_file_are_provided(args) - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -256,10 +289,7 @@ def unstake_nodes(args: Any): def unjail_nodes(args: Any): _check_if_either_bls_keys_or_validators_file_are_provided(args) - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -269,11 +299,69 @@ def unjail_nodes(args: Any): cli_shared.send_or_simulate(tx, args) +def delegate(args: Any): + ensure_arguments_are_provided_and_prepared(args) + + if not (int(args.value)): + raise errors.BadUrlError("Value not provided. Minimum value to delegate is 1 EGLD") + + sender = cli_shared.prepare_account(args) + config = TransactionsFactoryConfig(args.chain) + delegation = DelegationOperations(config) + + tx = delegation.prepare_transaction_for_delegating(sender, args) + cli_shared.send_or_simulate(tx, args) + + +def claim_rewards(args: Any): + ensure_arguments_are_provided_and_prepared(args) + + sender = cli_shared.prepare_account(args) + config = TransactionsFactoryConfig(args.chain) + delegation = DelegationOperations(config) + + tx = delegation.prepare_transaction_for_claiming_rewards(sender, args) + cli_shared.send_or_simulate(tx, args) + + +def redelegate_rewards(args: Any): + ensure_arguments_are_provided_and_prepared(args) + + sender = cli_shared.prepare_account(args) + config = TransactionsFactoryConfig(args.chain) + delegation = DelegationOperations(config) + + tx = delegation.prepare_transaction_for_redelegating_rewards(sender, args) + cli_shared.send_or_simulate(tx, args) + + +def undelegate(args: Any): + ensure_arguments_are_provided_and_prepared(args) + + if not (int(args.value)): + raise errors.BadUrlError("Value not provided. Minimum value to undelegate is 1 EGLD") + + sender = cli_shared.prepare_account(args) + config = TransactionsFactoryConfig(args.chain) + delegation = DelegationOperations(config) + + tx = delegation.prepare_transaction_for_undelegating(sender, args) + cli_shared.send_or_simulate(tx, args) + + +def withdraw(args: Any): + ensure_arguments_are_provided_and_prepared(args) + + sender = cli_shared.prepare_account(args) + config = TransactionsFactoryConfig(args.chain) + delegation = DelegationOperations(config) + + tx = delegation.prepare_transaction_for_withdrawing(sender, args) + cli_shared.send_or_simulate(tx, args) + + def change_service_fee(args: Any): - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -284,10 +372,7 @@ def change_service_fee(args: Any): def modify_delegation_cap(args: Any): - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -298,10 +383,7 @@ def modify_delegation_cap(args: Any): def automatic_activation(args: Any): - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -312,10 +394,7 @@ def automatic_activation(args: Any): def redelegate_cap(args: Any): - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -326,10 +405,7 @@ def redelegate_cap(args: Any): def set_metadata(args: Any): - cli_shared.check_guardian_and_options_args(args) - cli_shared.check_broadcast_args(args) - cli_shared.prepare_chain_id_in_args(args) - cli_shared.prepare_nonce_in_args(args) + ensure_arguments_are_provided_and_prepared(args) sender = cli_shared.prepare_account(args) config = TransactionsFactoryConfig(args.chain) @@ -337,3 +413,14 @@ def set_metadata(args: Any): tx = delegation.prepare_transaction_for_setting_metadata(sender, args) cli_shared.send_or_simulate(tx, args) + + +def make_new_contract_from_validator_data(args: Any): + ensure_arguments_are_provided_and_prepared(args) + + sender = cli_shared.prepare_account(args) + config = TransactionsFactoryConfig(args.chain) + delegation = DelegationOperations(config) + + tx = delegation.prepare_transaction_for_creating_delegation_contract_from_validator(sender, args) + cli_shared.send_or_simulate(tx, args) diff --git a/multiversx_sdk_cli/cli_dns.py b/multiversx_sdk_cli/cli_dns.py index 3e270595..80e93747 100644 --- a/multiversx_sdk_cli/cli_dns.py +++ b/multiversx_sdk_cli/cli_dns.py @@ -1,8 +1,6 @@ from typing import Any, List -from multiversx_sdk_core import Address -from multiversx_sdk_network_providers.proxy_network_provider import \ - ProxyNetworkProvider +from multiversx_sdk import Address, ProxyNetworkProvider from prettytable import PrettyTable from multiversx_sdk_cli import cli_shared @@ -11,6 +9,7 @@ dns_address_for_name, name_hash, register, registration_cost, resolve, validate_name, version) +from multiversx_sdk_cli.errors import ArgumentsNotProvidedError def setup_parser(args: List[str], subparsers: Any) -> Any: @@ -22,7 +21,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: cli_shared.add_broadcast_args(sub, relay=True) cli_shared.add_wallet_args(args, sub) cli_shared.add_proxy_arg(sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False) cli_shared.add_guardian_wallet_args(args, sub) sub.add_argument("--name", help="the name to register") sub.set_defaults(func=register) @@ -34,7 +33,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "dns", "validate-name", "Asks one of the DNS contracts to validate a name. Can be useful before registering it.") _add_name_arg(sub) - sub.add_argument("--shard-id", default=0, help="shard id of the contract to call (default: %(default)s)") + sub.add_argument("--shard-id", type=int, default=0, help="shard id of the contract to call (default: %(default)s)") cli_shared.add_proxy_arg(sub) sub.set_defaults(func=dns_validate_name) @@ -43,12 +42,12 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.set_defaults(func=get_name_hash) sub = cli_shared.add_command_subparser(subparsers, "dns", "registration-cost", "Gets the registration cost from a DNS smart contract, by default the one with shard id 0.") - sub.add_argument("--shard-id", default=0, help="shard id of the contract to call (default: %(default)s)") + sub.add_argument("--shard-id", type=int, default=0, help="shard id of the contract to call (default: %(default)s)") cli_shared.add_proxy_arg(sub) sub.set_defaults(func=get_registration_cost) sub = cli_shared.add_command_subparser(subparsers, "dns", "version", "Asks the contract for its version") - sub.add_argument("--shard-id", default=0, help="shard id of the contract to call (default: %(default)s)") + sub.add_argument("--shard-id", type=int, default=0, help="shard id of the contract to call (default: %(default)s)") sub.add_argument("--all", action="store_true", default=False, help="prints a list of all DNS contracts and their current versions (default: %(default)s)") cli_shared.add_proxy_arg(sub) sub.set_defaults(func=get_version) @@ -72,13 +71,21 @@ def _add_name_arg(sub: Any): sub.add_argument("name", help="the name for which to check") +def _ensure_proxy_is_provided(args: Any): + if not args.proxy: + raise ArgumentsNotProvidedError("'--proxy' argument not provided") + + def dns_resolve(args: Any): + _ensure_proxy_is_provided(args) + addr = resolve(args.name, ProxyNetworkProvider(args.proxy)) if addr.to_hex() != Address.new_from_bech32(ADDRESS_ZERO_BECH32).to_hex(): print(addr.to_bech32()) def dns_validate_name(args: Any): + _ensure_proxy_is_provided(args) validate_name(args.name, args.shard_id, ProxyNetworkProvider(args.proxy)) @@ -99,10 +106,13 @@ def get_dns_address_for_name_hex(args: Any): def get_registration_cost(args: Any): + _ensure_proxy_is_provided(args) print(registration_cost(args.shard_id, ProxyNetworkProvider(args.proxy))) def get_version(args: Any): + _ensure_proxy_is_provided(args) + proxy = ProxyNetworkProvider(args.proxy) if args.all: t = PrettyTable(['Shard ID', 'Contract address (bech32)', 'Contract address (hex)', 'Version']) diff --git a/multiversx_sdk_cli/cli_faucet.py b/multiversx_sdk_cli/cli_faucet.py new file mode 100644 index 00000000..878dc7ad --- /dev/null +++ b/multiversx_sdk_cli/cli_faucet.py @@ -0,0 +1,72 @@ +import logging +import webbrowser +from enum import Enum +from typing import Any, List, Tuple + +from multiversx_sdk_cli import cli_shared +from multiversx_sdk_cli.errors import BadUserInput +from multiversx_sdk_cli.native_auth_client import (NativeAuthClient, + NativeAuthClientConfig) + +logger = logging.getLogger("cli.faucet") + + +class WebWalletUrls(Enum): + DEVNET = "https://devnet-wallet.multiversx.com" + TESTNET = "https://testnet-wallet.multiversx.com" + + +class ApiUrls(Enum): + DEVNET = "https://devnet-api.multiversx.com" + TESTNET = "https://testnet-api.multiversx.com" + + +def setup_parser(args: List[str], subparsers: Any) -> Any: + parser = cli_shared.add_group_subparser(subparsers, "faucet", "Get xEGLD on Devnet or Testnet") + subparsers = parser.add_subparsers() + + sub = cli_shared.add_command_subparser(subparsers, "faucet", "request", "Request xEGLD.") + cli_shared.add_wallet_args(args, sub) + sub.add_argument("--chain", required=True, help="the chain identifier") + sub.set_defaults(func=faucet) + + parser.epilog = cli_shared.build_group_epilog(subparsers) + return subparsers + + +def faucet(args: Any): + account = cli_shared.prepare_account(args) + wallet, api = get_wallet_and_api_urls(args) + + config = NativeAuthClientConfig(origin=wallet, api_url=api) + client = NativeAuthClient(config) + + init_token = client.initialize() + token_for_siginig = f"{account.address.to_bech32()}{init_token}" + signature = account.sign_message(token_for_siginig.encode()) + + access_token = client.get_token( + address=account.address.to_bech32(), + token=init_token, + signature=signature + ) + + logger.info(f"Requesting funds for address: {account.address.to_bech32()}") + call_web_wallet_faucet(wallet_url=wallet, access_token=access_token) + + +def call_web_wallet_faucet(wallet_url: str, access_token: str): + faucet_url = f"{wallet_url}/faucet?accessToken={access_token}" + webbrowser.open_new_tab(faucet_url) + + +def get_wallet_and_api_urls(args: Any) -> Tuple[str, str]: + chain: str = args.chain + + if chain.upper() == "D": + return WebWalletUrls.DEVNET.value, ApiUrls.DEVNET.value + + if chain.upper() == "T": + return WebWalletUrls.TESTNET.value, ApiUrls.TESTNET.value + + raise BadUserInput("Invalid chain id. Choose between 'D' for devnet and 'T' for testnet.") diff --git a/multiversx_sdk_cli/cli_output.py b/multiversx_sdk_cli/cli_output.py index 91560bfd..bb2fdc94 100644 --- a/multiversx_sdk_cli/cli_output.py +++ b/multiversx_sdk_cli/cli_output.py @@ -3,8 +3,7 @@ from collections import OrderedDict from typing import Any, Dict, List, Optional, Union -from multiversx_sdk_network_providers.transactions import \ - transaction_to_dictionary +from multiversx_sdk import TransactionsConverter from multiversx_sdk_cli import utils from multiversx_sdk_cli.interfaces import IAddress, ITransaction @@ -52,7 +51,8 @@ def build(self) -> Dict[str, Any]: output: Dict[str, Any] = OrderedDict() if self.emitted_transaction: - emitted_transaction_dict = transaction_to_dictionary(self.emitted_transaction) + tx_converter = TransactionsConverter() + emitted_transaction_dict = tx_converter.transaction_to_dictionary(self.emitted_transaction) emitted_transaction_hash = self.emitted_transaction_hash or "" emitted_transaction_data = self.emitted_transaction.data.decode() utils.omit_fields(emitted_transaction_dict, self.emitted_transaction_omitted_fields) diff --git a/multiversx_sdk_cli/cli_shared.py b/multiversx_sdk_cli/cli_shared.py index 48d04f2f..867c2e1f 100644 --- a/multiversx_sdk_cli/cli_shared.py +++ b/multiversx_sdk_cli/cli_shared.py @@ -5,9 +5,7 @@ from argparse import FileType from typing import Any, Dict, List, Text, cast -from multiversx_sdk_core import Address -from multiversx_sdk_network_providers.proxy_network_provider import \ - ProxyNetworkProvider +from multiversx_sdk import Address, ProxyNetworkProvider from multiversx_sdk_cli import config, errors, utils from multiversx_sdk_cli.accounts import Account, LedgerAccount @@ -65,7 +63,13 @@ def add_command_subparser(subparsers: Any, group: str, command: str, description ) -def add_tx_args(args: List[str], sub: Any, with_nonce: bool = True, with_receiver: bool = True, with_data: bool = True, with_estimate_gas: bool = False, with_guardian: bool = False): +def add_tx_args( + args: List[str], + sub: Any, + with_nonce: bool = True, + with_receiver: bool = True, + with_data: bool = True, + with_estimate_gas: bool = False): if with_nonce: sub.add_argument("--nonce", type=int, required=not ("--recall-nonce" in args), help="# the nonce for the transaction") sub.add_argument("--recall-nonce", action="store_true", default=False, help="⭮ whether to recall the nonce when creating the transaction (default: %(default)s)") @@ -87,12 +91,18 @@ def add_tx_args(args: List[str], sub: Any, with_nonce: bool = True, with_receive sub.add_argument("--chain", help="the chain identifier") sub.add_argument("--version", type=int, default=DEFAULT_TX_VERSION, help="the transaction version (default: %(default)s)") - if with_guardian: - add_guardian_args(sub) + add_guardian_args(sub) + add_relayed_v3_args(sub) sub.add_argument("--options", type=int, default=0, help="the transaction options (default: 0)") +def add_relayed_v3_args(sub: Any): + sub.add_argument("--relayer", help="the address of the relayer") + sub.add_argument("--inner-transactions", help="a json file containing the inner transactions; should only be provided when creating the relayer's transaction") + sub.add_argument("--inner-transactions-outfile", type=str, help="where to save the transaction as an inner transaction (default: stdout)") + + def add_guardian_args(sub: Any): sub.add_argument("--guardian", type=str, help="the address of the guradian", default="") sub.add_argument("--guardian-service-url", type=str, help="the url of the guardian service", default="") diff --git a/multiversx_sdk_cli/cli_transactions.py b/multiversx_sdk_cli/cli_transactions.py index 5e02cac1..924e4c9d 100644 --- a/multiversx_sdk_cli/cli_transactions.py +++ b/multiversx_sdk_cli/cli_transactions.py @@ -1,15 +1,21 @@ +import logging from pathlib import Path -from typing import Any, List +from typing import Any, Dict, List + +from multiversx_sdk import Transaction, TransactionsConverter from multiversx_sdk_cli import cli_shared, utils from multiversx_sdk_cli.cli_output import CLIOutputBuilder from multiversx_sdk_cli.cosign_transaction import cosign_transaction from multiversx_sdk_cli.custom_network_provider import CustomNetworkProvider -from multiversx_sdk_cli.errors import NoWalletProvided +from multiversx_sdk_cli.errors import BadUsage, NoWalletProvided from multiversx_sdk_cli.transactions import (compute_relayed_v1_data, do_prepare_transaction, + load_inner_transactions_from_file, load_transaction_from_file) +logger = logging.getLogger("cli.transactions") + def setup_parser(args: List[str], subparsers: Any) -> Any: parser = cli_shared.add_group_subparser(subparsers, "tx", "Create and broadcast Transactions") @@ -17,6 +23,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub = cli_shared.add_command_subparser(subparsers, "tx", "new", f"Create a new transaction.{CLIOutputBuilder.describe()}") _add_common_arguments(args, sub) + _add_token_transfers_args(sub) cli_shared.add_outfile_arg(sub, what="signed transaction, hash") cli_shared.add_broadcast_args(sub, relay=True) cli_shared.add_proxy_arg(sub) @@ -57,10 +64,16 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: def _add_common_arguments(args: List[str], sub: Any): cli_shared.add_wallet_args(args, sub) - cli_shared.add_tx_args(args, sub, with_guardian=True) + cli_shared.add_tx_args(args, sub) sub.add_argument("--data-file", type=str, default=None, help="a file containing transaction data") +def _add_token_transfers_args(sub: Any): + sub.add_argument("--token-transfers", nargs='+', + help="token transfers for transfer & execute, as [token, amount] " + "E.g. --token-transfers NFT-123456-0a 1 ESDT-987654 100000000") + + def create_transaction(args: Any): args = utils.as_object(args) @@ -72,15 +85,46 @@ def create_transaction(args: Any): if args.data_file: args.data = Path(args.data_file).read_text() + check_relayer_transaction_with_data_field_for_relayed_v3(args) + tx = do_prepare_transaction(args) + if hasattr(args, "inner_transactions_outfile") and args.inner_transactions_outfile: + save_transaction_to_inner_transactions_file(tx, args) + return + if hasattr(args, "relay") and args.relay: + logger.warning("RelayedV1 transactions are deprecated. Please use RelayedV3 instead.") args.outfile.write(compute_relayed_v1_data(tx)) return cli_shared.send_or_simulate(tx, args) +def save_transaction_to_inner_transactions_file(transaction: Transaction, args: Any): + inner_txs_file = Path(args.inner_transactions_outfile).expanduser() + transactions = get_inner_transactions_if_any(inner_txs_file) + transactions.append(transaction) + + tx_converter = TransactionsConverter() + inner_transactions: Dict[str, Any] = {} + inner_transactions["innerTransactions"] = [tx_converter.transaction_to_dictionary(tx) for tx in transactions] + + with open(inner_txs_file, "w") as file: + utils.dump_out_json(inner_transactions, file) + + +def get_inner_transactions_if_any(file: Path) -> List[Transaction]: + if file.is_file(): + return load_inner_transactions_from_file(file) + return [] + + +def check_relayer_transaction_with_data_field_for_relayed_v3(args: Any): + if hasattr(args, "inner_transactions") and args.inner_transactions and args.data: + raise BadUsage("Can't set data field when creating a relayedV3 transaction") + + def send_transaction(args: Any): args = utils.as_object(args) diff --git a/multiversx_sdk_cli/cli_validators.py b/multiversx_sdk_cli/cli_validators.py index e2a73b54..106f403c 100644 --- a/multiversx_sdk_cli/cli_validators.py +++ b/multiversx_sdk_cli/cli_validators.py @@ -88,7 +88,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: def _add_common_arguments(args: List[str], sub: Any): cli_shared.add_proxy_arg(sub) cli_shared.add_wallet_args(args, sub) - cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_estimate_gas=True, with_guardian=True) + cli_shared.add_tx_args(args, sub, with_receiver=False, with_data=False, with_estimate_gas=True) cli_shared.add_broadcast_args(sub, relay=False) cli_shared.add_outfile_arg(sub, what="signed transaction, hash") cli_shared.add_guardian_wallet_args(args, sub) diff --git a/multiversx_sdk_cli/cli_wallet.py b/multiversx_sdk_cli/cli_wallet.py index de7dd7ce..eeae9ebb 100644 --- a/multiversx_sdk_cli/cli_wallet.py +++ b/multiversx_sdk_cli/cli_wallet.py @@ -5,14 +5,14 @@ from pathlib import Path from typing import Any, List, Optional, Tuple -from multiversx_sdk_core import Address -from multiversx_sdk_wallet import UserSecretKey, UserWallet -from multiversx_sdk_wallet.mnemonic import Mnemonic -from multiversx_sdk_wallet.user_pem import UserPEM +from multiversx_sdk import (Address, Mnemonic, UserPEM, UserSecretKey, + UserWallet) +from multiversx_sdk.core.address import get_shard_of_pubkey from multiversx_sdk_cli import cli_shared, utils -from multiversx_sdk_cli.constants import DEFAULT_HRP -from multiversx_sdk_cli.errors import KnownError +from multiversx_sdk_cli.constants import DEFAULT_HRP, NUMBER_OF_SHARDS +from multiversx_sdk_cli.errors import (BadUserInput, KnownError, + WalletGenerationError) from multiversx_sdk_cli.sign_verify import SignedMessage, sign_message from multiversx_sdk_cli.ux import show_critical_error, show_message @@ -34,6 +34,9 @@ WALLET_FORMATS_AND_ADDRESSES = [*WALLET_FORMATS, WALLET_FORMAT_ADDRESS_BECH32, WALLET_FORMAT_ADDRESS_HEX] +MAX_ITERATIONS_FOR_GENERATING_WALLET = 100 +CURRENT_SHARDS = [i for i in range(NUMBER_OF_SHARDS)] + def setup_parser(args: List[str], subparsers: Any) -> Any: parser = cli_shared.add_group_subparser( @@ -52,6 +55,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any: sub.add_argument("--format", choices=WALLET_FORMATS, help="the format of the generated wallet file (default: %(default)s)", default=None) sub.add_argument("--outfile", help="the output path and base file name for the generated wallet files (default: %(default)s)", type=str) sub.add_argument("--address-hrp", help=f"the human-readable part of the address, when format is {WALLET_FORMAT_KEYSTORE_SECRET_KEY} or {WALLET_FORMAT_PEM} (default: %(default)s)", type=str, default=DEFAULT_HRP) + sub.add_argument("--shard", type=int, help="the shard in which the address will be generated; (default: random)") sub.set_defaults(func=wallet_new) sub = cli_shared.add_command_subparser( @@ -109,8 +113,13 @@ def wallet_new(args: Any): format = args.format outfile = args.outfile address_hrp = args.address_hrp + shard = args.shard + + if shard is not None: + mnemonic = _generate_mnemonic_with_shard_constraint(shard) + else: + mnemonic = Mnemonic.generate() - mnemonic = Mnemonic.generate() print(f"Mnemonic: {mnemonic.get_text()}") print(f"Wallet address: {mnemonic.derive_key().generate_public_key().to_address(address_hrp).to_bech32()}") @@ -146,6 +155,22 @@ def wallet_new(args: Any): logger.info(f"Wallet ({format}) saved: {outfile}") +def _generate_mnemonic_with_shard_constraint(shard: int) -> Mnemonic: + + if shard not in CURRENT_SHARDS: + raise BadUserInput(f"Wrong shard provided. Choose between {CURRENT_SHARDS}") + + for _ in range(MAX_ITERATIONS_FOR_GENERATING_WALLET): + mnemonic = Mnemonic.generate() + pubkey = mnemonic.derive_key().generate_public_key() + generated_address_shard = get_shard_of_pubkey(pubkey.buffer, NUMBER_OF_SHARDS) + + if shard == generated_address_shard: + return mnemonic + + raise WalletGenerationError(f"Couldn't generate wallet in shard {shard}") + + def convert_wallet(args: Any): infile = Path(args.infile).expanduser().resolve() if args.infile else None outfile = Path(args.outfile).expanduser().resolve() if args.outfile else None @@ -160,7 +185,7 @@ def convert_wallet(args: Any): if infile: input_text = infile.read_text() else: - print(f"Insert text below. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done.") + print("Insert text below. Press 'Ctrl-D' (Linux / MacOS) or 'Ctrl-Z' (Windows) when done.") input_text = sys.stdin.read().strip() mnemonic, secret_key = _load_wallet(input_text, in_format, address_index) diff --git a/multiversx_sdk_cli/config.py b/multiversx_sdk_cli/config.py index 9230dcd2..3b54049f 100644 --- a/multiversx_sdk_cli/config.py +++ b/multiversx_sdk_cli/config.py @@ -3,7 +3,6 @@ from typing import Any, Dict, List from multiversx_sdk_cli import errors, utils -from multiversx_sdk_cli.ux import show_warning SDK_PATH = Path("~/multiversx-sdk").expanduser().resolve() LOCAL_CONFIG_PATH = Path("mxpy.json").resolve() @@ -33,7 +32,7 @@ class MetaChainSystemSCsCost: def get_dependency_resolution(key: str) -> str: try: return get_value(f"dependencies.{key}.resolution") - except: + except Exception: return "" @@ -202,24 +201,11 @@ def add_config_args(argv: List[str]) -> List[str]: except KeyError: return argv - check_for_deprecated_args(config_args) - final_args = determine_final_args(argv, config_args) print(f"Found extra arguments in mxpy.json. Final arguments: {final_args}") return final_args -def check_for_deprecated_args(args: List[str]) -> None: - if "proxy" in args: - show_warning("Providing `proxy` in the configuration file is deprecated. It will not be used. Please remove it!") - - if "chainID" in args: - show_warning("Providing `chainID` in the configuration file is deprecated. It will not be used. Please remove it!") - - if "txVersion" in args: - show_warning("Providing `txVersion` in the configuration file is deprecated. It will not be used. Please remove it!") - - def determine_final_args(argv: List[str], config_args: Dict[str, Any]) -> List[str]: extra_args: List[str] = [] for key, value in config_args.items(): diff --git a/multiversx_sdk_cli/contract_verification.py b/multiversx_sdk_cli/contract_verification.py index 7c6266da..cbad2d3d 100644 --- a/multiversx_sdk_cli/contract_verification.py +++ b/multiversx_sdk_cli/contract_verification.py @@ -6,7 +6,7 @@ from typing import Any, Dict, Optional, Tuple import requests -from multiversx_sdk_core import Address +from multiversx_sdk import Address from multiversx_sdk_cli.accounts import Account from multiversx_sdk_cli.errors import KnownError @@ -112,7 +112,7 @@ def _create_request_signature(account: Account, contract_address: Address, reque def query_status_with_task_id(url: str, task_id: str, interval: int = 10): - logger.info(f"Please wait while we verify your contract. This may take a while.") + logger.info("Please wait while we verify your contract. This may take a while.") old_status = "" while True: @@ -120,7 +120,7 @@ def query_status_with_task_id(url: str, task_id: str, interval: int = 10): status = response.get("status", "") if status == "finished": - logger.info(f"Verification finished!") + logger.info("Verification finished!") dump_out_json(response) break elif status != old_status: diff --git a/multiversx_sdk_cli/contracts.py b/multiversx_sdk_cli/contracts.py index a0a18855..98885dc4 100644 --- a/multiversx_sdk_cli/contracts.py +++ b/multiversx_sdk_cli/contracts.py @@ -1,20 +1,18 @@ -import base64 import logging from pathlib import Path -from typing import Any, List, Optional, Protocol, Sequence, Union +from typing import Any, List, Optional, Protocol, Union -from multiversx_sdk_core import (Token, TokenComputer, TokenTransfer, - Transaction, TransactionPayload) -from multiversx_sdk_core.address import Address -from multiversx_sdk_core.transaction_factories import \ - SmartContractTransactionsFactory -from multiversx_sdk_network_providers.interface import IContractQuery +from multiversx_sdk import (Address, QueryRunnerAdapter, + SmartContractQueriesController, + SmartContractTransactionsFactory, Token, + TokenComputer, TokenTransfer, Transaction, + TransactionPayload) +from multiversx_sdk.abi import Abi from multiversx_sdk_cli import errors from multiversx_sdk_cli.accounts import Account from multiversx_sdk_cli.constants import DEFAULT_HRP from multiversx_sdk_cli.interfaces import IAddress -from multiversx_sdk_cli.utils import Object logger = logging.getLogger("contracts") @@ -29,57 +27,34 @@ def query_contract(self, query: Any) -> 'IContractQueryResponse': ... -class QueryResult(Object): - def __init__(self, as_base64: str, as_hex: str, as_number: Optional[int]): - self.base64 = as_base64 - self.hex = as_hex - self.number = as_number - - -class ContractQuery(IContractQuery): - def __init__(self, address: IAddress, function: str, value: int, arguments: List[bytes], caller: Optional[IAddress] = None): - self.contract = address - self.function = function - self.caller = caller - self.value = value - self.encoded_arguments = [item.hex() for item in arguments] - - def get_contract(self) -> IAddress: - return self.contract - - def get_function(self) -> str: - return self.function - - def get_encoded_arguments(self) -> Sequence[str]: - return self.encoded_arguments - - def get_caller(self) -> Optional[IAddress]: - return self.caller - - def get_value(self) -> int: - return self.value - - class IContractQueryResponse(Protocol): return_data: List[str] return_code: str return_message: str + gas_used: int + + def get_return_data_parts(self) -> List[bytes]: + ... class IConfig(Protocol): chain_id: str min_gas_limit: int gas_limit_per_byte: int + gas_limit_claim_developer_rewards: int + gas_limit_change_owner_address: int class SmartContract: - def __init__(self, config: IConfig): - self._factory = SmartContractTransactionsFactory(config, TokenComputer()) + def __init__(self, config: IConfig, abi: Optional[Abi] = None): + self._abi = abi + self._factory = SmartContractTransactionsFactory(config, abi) def prepare_deploy_transaction(self, owner: Account, bytecode: Path, - arguments: Union[List[str], None], + arguments: Union[List[Any], None], + should_prepare_args: bool, upgradeable: bool, readable: bool, payable: bool, @@ -90,7 +65,9 @@ def prepare_deploy_transaction(self, version: int, options: int, guardian: str) -> Transaction: - args = prepare_args_for_factory(arguments) if arguments else [] + args = arguments if arguments else [] + if should_prepare_args: + args = self._prepare_args_for_factory(args) tx = self._factory.create_transaction_for_deploy( sender=owner.address, @@ -115,7 +92,8 @@ def prepare_execute_transaction(self, caller: Account, contract: Address, function: str, - arguments: Union[List[str], None], + arguments: Union[List[Any], None], + should_prepare_args: bool, gas_limit: int, value: int, transfers: Union[List[str], None], @@ -124,7 +102,10 @@ def prepare_execute_transaction(self, options: int, guardian: str) -> Transaction: token_transfers = self._prepare_token_transfers(transfers) if transfers else [] - args = prepare_args_for_factory(arguments) if arguments else [] + + args = arguments if arguments else [] + if should_prepare_args: + args = self._prepare_args_for_factory(args) tx = self._factory.create_transaction_for_execute( sender=caller.address, @@ -148,6 +129,7 @@ def prepare_upgrade_transaction(self, contract: IAddress, bytecode: Path, arguments: Union[List[str], None], + should_prepare_args: bool, upgradeable: bool, readable: bool, payable: bool, @@ -158,7 +140,9 @@ def prepare_upgrade_transaction(self, version: int, options: int, guardian: str) -> Transaction: - args = prepare_args_for_factory(arguments) if arguments else [] + args = arguments if arguments else [] + if should_prepare_args: + args = self._prepare_args_for_factory(args) tx = self._factory.create_transaction_for_upgrade( sender=owner.address, @@ -180,6 +164,30 @@ def prepare_upgrade_transaction(self, return tx + def query_contract(self, + contract_address: IAddress, + proxy: INetworkProvider, + function: str, + arguments: Optional[List[Any]], + should_prepare_args: bool) -> List[Any]: + args = arguments if arguments else [] + if should_prepare_args: + args = self._prepare_args_for_factory(args) + + query_runner = QueryRunnerAdapter(proxy) + sc_query_controller = SmartContractQueriesController(query_runner, self._abi) + + try: + response = sc_query_controller.query( + contract=contract_address.to_bech32(), + function=function, + arguments=args + ) + except Exception as e: + raise errors.QueryContractError("Couldn't query contract: ", e) + + return response + def _prepare_token_transfers(self, transfers: List[str]) -> List[TokenTransfer]: token_computer = TokenComputer() token_transfers: List[TokenTransfer] = [] @@ -195,64 +203,32 @@ def _prepare_token_transfers(self, transfers: List[str]) -> List[TokenTransfer]: return token_transfers - -def query_contract( - contract_address: IAddress, - proxy: INetworkProvider, - function: str, - arguments: List[Any], - value: int = 0, - caller: Optional[IAddress] = None -) -> List[Any]: - response_data = query_detailed(contract_address, proxy, function, arguments, value, caller) - return_data = response_data.return_data - return [_interpret_return_data(data) for data in return_data] - - -def query_detailed(contract_address: IAddress, proxy: INetworkProvider, function: str, arguments: List[Any], - value: int = 0, caller: Optional[IAddress] = None) -> Any: - arguments = arguments or [] - # Temporary workaround, until we use sdk-core's serializer. - arguments_hex = [_prepare_argument(arg) for arg in arguments] - prepared_arguments_bytes = [bytes.fromhex(arg) for arg in arguments_hex] - - query = ContractQuery(contract_address, function, value, prepared_arguments_bytes, caller) - - response = proxy.query_contract(query) - # Temporary workaround, until we add "isSuccess" on the response class. - if response.return_code != "ok": - raise RuntimeError(f"Query failed: {response.return_message}") - return response - - -def _interpret_return_data(data: str) -> Any: - if not data: - return data - - try: - as_bytes = base64.b64decode(data) - as_hex = as_bytes.hex() - as_number = _interpret_as_number_if_safely(as_hex) - - result = QueryResult(data, as_hex, as_number) - return result - except Exception: - logger.warn(f"Cannot interpret return data: {data}") - return None - - -def _interpret_as_number_if_safely(as_hex: str) -> Optional[int]: - """ - Makes sure the string can be safely converted to an int (and then back to a string). - - See: - - https://stackoverflow.com/questions/73693104/valueerror-exceeds-the-limit-4300-for-integer-string-conversion - - https://github.com/python/cpython/issues/95778 - """ - try: - return int(str(int(as_hex or "0", 16))) - except: - return None + def _prepare_args_for_factory(self, arguments: List[str]) -> List[Any]: + args: List[Any] = [] + + for arg in arguments: + if arg.startswith(HEX_PREFIX): + args.append(self._hex_to_bytes(arg)) + elif arg.isnumeric(): + args.append(int(arg)) + elif arg.startswith(DEFAULT_HRP): + args.append(Address.new_from_bech32(arg)) + elif arg.lower() == FALSE_STR_LOWER: + args.append(False) + elif arg.lower() == TRUE_STR_LOWER: + args.append(True) + elif arg.startswith(STR_PREFIX): + args.append(arg[len(STR_PREFIX):]) + else: + raise errors.BadUserInput(f"Unknown argument type for argument: `{arg}`. Use `mxpy contract --help` to check all supported arguments") + + return args + + def _hex_to_bytes(self, arg: str): + argument = arg[len(HEX_PREFIX):] + argument = argument.upper() + argument = ensure_even_length(argument) + return bytes.fromhex(argument) def prepare_execute_transaction_data(function: str, arguments: List[Any]) -> TransactionPayload: @@ -264,36 +240,7 @@ def prepare_execute_transaction_data(function: str, arguments: List[Any]) -> Tra return TransactionPayload.from_str(tx_data) -def prepare_args_for_factory(arguments: List[str]) -> List[Any]: - args: List[Any] = [] - - for arg in arguments: - if arg.startswith(HEX_PREFIX): - args.append(hex_to_bytes(arg)) - elif arg.isnumeric(): - args.append(int(arg)) - elif arg.startswith(DEFAULT_HRP): - args.append(Address.new_from_bech32(arg)) - elif arg.lower() == FALSE_STR_LOWER: - args.append(False) - elif arg.lower() == TRUE_STR_LOWER: - args.append(True) - elif arg.startswith(STR_PREFIX): - args.append(arg[len(STR_PREFIX):]) - else: - raise errors.BadUserInput(f"Unknown argument type for argument: `{arg}`. Use `mxpy contract --help` to check all supported arguments") - - return args - - -def hex_to_bytes(arg: str): - argument = arg[len(HEX_PREFIX):] - argument = argument.upper() - argument = ensure_even_length(argument) - return bytes.fromhex(argument) - - -# only used for contract queries and stake operations +# only used for stake operations def _prepare_argument(argument: Any): as_str = str(argument) as_hex = _to_hex(as_str) diff --git a/multiversx_sdk_cli/cosign_transaction.py b/multiversx_sdk_cli/cosign_transaction.py index 30a3ec79..fce9e274 100644 --- a/multiversx_sdk_cli/cosign_transaction.py +++ b/multiversx_sdk_cli/cosign_transaction.py @@ -1,17 +1,17 @@ from typing import Any, Dict import requests -from multiversx_sdk_network_providers.transactions import \ - transaction_to_dictionary +from multiversx_sdk import TransactionsConverter from multiversx_sdk_cli.errors import GuardianServiceError from multiversx_sdk_cli.interfaces import ITransaction def cosign_transaction(transaction: ITransaction, service_url: str, guardian_code: str) -> ITransaction: + tx_converter = TransactionsConverter() payload = { "code": f"{guardian_code}", - "transaction": transaction_to_dictionary(transaction) + "transaction": tx_converter.transaction_to_dictionary(transaction) } url = f"{service_url}/sign-transaction" diff --git a/multiversx_sdk_cli/custom_network_provider.py b/multiversx_sdk_cli/custom_network_provider.py index 3354a495..e11c9870 100644 --- a/multiversx_sdk_cli/custom_network_provider.py +++ b/multiversx_sdk_cli/custom_network_provider.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional, Protocol -from multiversx_sdk_network_providers import GenericError, ProxyNetworkProvider +from multiversx_sdk import GenericError, ProxyNetworkProvider from multiversx_sdk_cli.errors import ProxyError from multiversx_sdk_cli.interfaces import ISimulateResponse, ITransaction diff --git a/multiversx_sdk_cli/delegation/staking_provider.py b/multiversx_sdk_cli/delegation/staking_provider.py index 3152925e..418418dc 100644 --- a/multiversx_sdk_cli/delegation/staking_provider.py +++ b/multiversx_sdk_cli/delegation/staking_provider.py @@ -1,13 +1,10 @@ from pathlib import Path from typing import Any, List, Protocol, Tuple -from multiversx_sdk_core import Address -from multiversx_sdk_core.transaction_factories import \ - DelegationTransactionsFactory -from multiversx_sdk_wallet import ValidatorPublicKey +from multiversx_sdk import (Address, DelegationTransactionsFactory, + Transaction, ValidatorPublicKey) +from multiversx_sdk.core.serializer import args_to_string -from multiversx_sdk_cli.accounts import Account, LedgerAccount -from multiversx_sdk_cli.cli_password import load_password from multiversx_sdk_cli.errors import BadUsage from multiversx_sdk_cli.interfaces import IAddress, ITransaction from multiversx_sdk_cli.validators.validators_file import ValidatorsFile @@ -174,11 +171,13 @@ def prepare_transaction_for_unjailing_nodes(self, owner: IAccount, args: Any) -> delegation_contract = Address.new_from_bech32(args.delegation_contract) public_keys = self._load_validators_public_keys(args) + amount = int(args.value) tx = self._factory.create_transaction_for_unjailing_nodes( sender=owner.address, delegation_contract=delegation_contract, - public_keys=public_keys + public_keys=public_keys, + amount=amount ) tx.nonce = int(args.nonce) tx.version = int(args.version) @@ -193,6 +192,103 @@ def prepare_transaction_for_unjailing_nodes(self, owner: IAccount, args: Any) -> return tx + def prepare_transaction_for_delegating(self, owner: IAccount, args: Any) -> ITransaction: + delegation_contract = Address.new_from_bech32(args.delegation_contract) + + tx = self._factory.create_transaction_for_delegating( + sender=owner.address, + delegation_contract=delegation_contract, + amount=int(args.value) + ) + tx.nonce = int(args.nonce) + tx.version = int(args.version) + tx.options = int(args.options) + tx.guardian = args.guardian + + if args.gas_limit: + tx.gas_limit = int(args.gas_limit) + + tx.signature = bytes.fromhex(owner.sign_transaction(tx)) + + return tx + + def prepare_transaction_for_claiming_rewards(self, owner: IAccount, args: Any) -> ITransaction: + delegation_contract = Address.new_from_bech32(args.delegation_contract) + + tx = self._factory.create_transaction_for_claiming_rewards( + sender=owner.address, + delegation_contract=delegation_contract + ) + tx.nonce = int(args.nonce) + tx.version = int(args.version) + tx.options = int(args.options) + tx.guardian = args.guardian + + if args.gas_limit: + tx.gas_limit = int(args.gas_limit) + + tx.signature = bytes.fromhex(owner.sign_transaction(tx)) + + return tx + + def prepare_transaction_for_redelegating_rewards(self, owner: IAccount, args: Any) -> ITransaction: + delegation_contract = Address.new_from_bech32(args.delegation_contract) + + tx = self._factory.create_transaction_for_redelegating_rewards( + sender=owner.address, + delegation_contract=delegation_contract + ) + tx.nonce = int(args.nonce) + tx.version = int(args.version) + tx.options = int(args.options) + tx.guardian = args.guardian + + if args.gas_limit: + tx.gas_limit = int(args.gas_limit) + + tx.signature = bytes.fromhex(owner.sign_transaction(tx)) + + return tx + + def prepare_transaction_for_undelegating(self, owner: IAccount, args: Any) -> ITransaction: + delegation_contract = Address.new_from_bech32(args.delegation_contract) + + tx = self._factory.create_transaction_for_undelegating( + sender=owner.address, + delegation_contract=delegation_contract, + amount=int(args.value) + ) + tx.nonce = int(args.nonce) + tx.version = int(args.version) + tx.options = int(args.options) + tx.guardian = args.guardian + + if args.gas_limit: + tx.gas_limit = int(args.gas_limit) + + tx.signature = bytes.fromhex(owner.sign_transaction(tx)) + + return tx + + def prepare_transaction_for_withdrawing(self, owner: IAccount, args: Any) -> ITransaction: + delegation_contract = Address.new_from_bech32(args.delegation_contract) + + tx = self._factory.create_transaction_for_withdrawing( + sender=owner.address, + delegation_contract=delegation_contract + ) + tx.nonce = int(args.nonce) + tx.version = int(args.version) + tx.options = int(args.options) + tx.guardian = args.guardian + + if args.gas_limit: + tx.gas_limit = int(args.gas_limit) + + tx.signature = bytes.fromhex(owner.sign_transaction(tx)) + + return tx + def prepare_transaction_for_changing_service_fee(self, owner: IAccount, args: Any) -> ITransaction: delegation_contract = Address.new_from_bech32(args.delegation_contract) @@ -311,6 +407,31 @@ def prepare_transaction_for_setting_metadata(self, owner: IAccount, args: Any) - return tx + def prepare_transaction_for_creating_delegation_contract_from_validator(self, owner: IAccount, args: Any) -> ITransaction: + receiver = "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6" + max_cap = int(args.max_cap) + fee = int(args.fee) + data = "makeNewContractFromValidatorData@" + args_to_string([max_cap, fee]) + + tx = Transaction( + sender=owner.address.to_bech32(), + receiver=receiver, + gas_limit=510000000, + chain_id=self._factory.config.chain_id, + data=data.encode(), + nonce=int(args.nonce), + version=int(args.version), + options=int(args.options), + guardian=args.guardian + ) + + if args.gas_limit: + tx.gas_limit = int(args.gas_limit) + + tx.signature = bytes.fromhex(owner.sign_transaction(tx)) + + return tx + def _load_validators_public_keys(self, args: Any) -> List[ValidatorPublicKey]: if args.bls_keys: return self._parse_public_bls_keys(args.bls_keys) @@ -333,7 +454,7 @@ def _get_public_keys_and_signed_messages(self, args: Any) -> Tuple[List[Validato validators_file = ValidatorsFile(validators_file_path) signers = validators_file.load_signers() - pubkey = self._get_pubkey_to_be_signed(args) + pubkey = Address.new_from_bech32(args.delegation_contract).get_public_key() public_keys: List[ValidatorPublicKey] = [] signed_messages: List[bytes] = [] @@ -344,17 +465,3 @@ def _get_public_keys_and_signed_messages(self, args: Any) -> Tuple[List[Validato signed_messages.append(signed_message) return public_keys, signed_messages - - def _get_pubkey_to_be_signed(self, args: Any) -> bytes: - account = Account() - if args.using_delegation_manager: - account = Account(address=Address.new_from_bech32(args.delegation_contract)) - elif args.pem: - account = Account(pem_file=args.pem) - elif args.keyfile: - password = load_password(args) - account = Account(key_file=args.keyfile, password=password) - elif args.ledger: - account = LedgerAccount(account_index=args.ledger_account_index, address_index=args.ledger_address_index) - - return account.address.get_public_key() diff --git a/multiversx_sdk_cli/dependencies/install.py b/multiversx_sdk_cli/dependencies/install.py index 0ca7d900..61bf0fe4 100644 --- a/multiversx_sdk_cli/dependencies/install.py +++ b/multiversx_sdk_cli/dependencies/install.py @@ -5,8 +5,7 @@ from multiversx_sdk_cli import config, errors from multiversx_sdk_cli.dependencies.modules import (DependencyModule, GolangModule, Rust, - TestWalletsModule, - VMToolsModule) + TestWalletsModule) logger = logging.getLogger("install") @@ -51,7 +50,6 @@ def get_all_deps() -> List[DependencyModule]: return [ Rust(key="rust"), GolangModule(key="golang"), - VMToolsModule(key="vmtools"), TestWalletsModule(key="testwallets") ] diff --git a/multiversx_sdk_cli/dependencies/modules.py b/multiversx_sdk_cli/dependencies/modules.py index 8814864c..3455b24d 100644 --- a/multiversx_sdk_cli/dependencies/modules.py +++ b/multiversx_sdk_cli/dependencies/modules.py @@ -1,5 +1,6 @@ import logging import os +import platform import shutil from os import path from pathlib import Path @@ -9,7 +10,7 @@ myprocess, utils, workstation) from multiversx_sdk_cli.dependencies.resolution import ( DependencyResolution, get_dependency_resolution) -from multiversx_sdk_cli.ux import show_warning +from multiversx_sdk_cli.ux import show_message, show_warning logger = logging.getLogger("modules") @@ -135,59 +136,6 @@ def _get_archive_path(self, tag: str) -> Path: return archive -class VMToolsModule(StandaloneModule): - def __init__(self, key: str, aliases: List[str] = []): - super().__init__(key, aliases) - self.repo_name = 'mx-chain-vm-go' - self.organisation = 'multiversx' - - def _post_install(self, tag: str): - dependencies.install_module('golang') - - self.build_binary(tag, 'test') - self.make_binary_symlink_in_parent_folder(tag, 'test', 'run-scenarios') - self.copy_libwasmer_in_parent_directory(tag) - - def build_binary(self, tag: str, binary_name: str): - source_folder = self.binary_source_folder(tag, binary_name) - golang = dependencies.get_module_by_key("golang") - golang_env = golang.get_env() - myprocess.run_process(['go', 'build'], cwd=source_folder, env=golang_env) - - def binary_source_folder(self, tag: str, binary_name: str): - directory = self.get_source_directory(tag) - return directory / 'cmd' / binary_name - - def make_binary_symlink_in_parent_folder(self, tag: str, binary_name: str, symlink_name: str): - source_folder = self.binary_source_folder(tag, binary_name) - binary = source_folder / binary_name - - parent = self.get_parent_directory() - symlink = parent / symlink_name - - symlink.unlink(missing_ok=True) - symlink.symlink_to(binary) - - def copy_libwasmer_in_parent_directory(self, tag: str): - libwasmer_directory = self.get_source_directory(tag) / 'wasmer' - cmd_test_directory = self.get_source_directory(tag) / 'cmd' / 'test' - parent_directory = self.get_parent_directory() - for f in libwasmer_directory.iterdir(): - if f.suffix in ['.dylib', '.so', '.dll']: - # Copy the dynamic library near the "run-scenarios" symlink - shutil.copy(f, parent_directory) - # Though, also copy the dynamic library near the target executable (seems to be necessary on MacOS) - shutil.copy(f, cmd_test_directory) - - def get_env(self) -> Dict[str, str]: - return dict() - - def get_source_directory(self, tag: str) -> Path: - directory = self.get_directory(tag) - first_subdirectory = next(directory.iterdir()) - return first_subdirectory - - class GolangModule(StandaloneModule): def _post_install(self, tag: str): parent_directory = self.get_parent_directory() @@ -244,11 +192,13 @@ def is_installed(self, tag: str) -> bool: which_rustc = shutil.which("rustc") which_cargo = shutil.which("cargo") which_sc_meta = shutil.which("sc-meta") + which_mx_scenario_go = shutil.which("mx-scenario-go") which_wasm_opt = shutil.which("wasm-opt") which_twiggy = shutil.which("twiggy") logger.info(f"which rustc: {which_rustc}") logger.info(f"which cargo: {which_cargo}") logger.info(f"which sc-meta: {which_sc_meta}") + logger.info(f"which mx-scenario-go: {which_mx_scenario_go}") logger.info(f"which wasm-opt: {which_wasm_opt}") logger.info(f"which twiggy: {which_twiggy}") @@ -291,6 +241,7 @@ def install(self, overwrite: bool) -> None: self._install_sc_meta() self._install_wasm_opt() self._install_twiggy() + show_message("To ensure sc-meta functions correctly, please install all the required dependencies by executing the following command: `sc-meta install all`.") def _check_install_env(self, apply_correction: bool = True): """ @@ -304,7 +255,7 @@ def _check_install_env(self, apply_correction: bool = True): This may cause problems with the installation.""") if apply_correction: - show_warning(f"CARGO_HOME will be temporarily unset.") + show_warning("CARGO_HOME will be temporarily unset.") os.environ["CARGO_HOME"] = "" if current_rustup_home: @@ -312,7 +263,7 @@ def _check_install_env(self, apply_correction: bool = True): This may cause problems with the installation of rust.""") if apply_correction: - show_warning(f"RUSTUP_HOME will be temporarily unset.") + show_warning("RUSTUP_HOME will be temporarily unset.") os.environ["RUSTUP_HOME"] = "" def _install_rust(self, tag: str) -> None: @@ -359,6 +310,18 @@ def _install_twiggy(self): myprocess.run_process(args, env=self.get_cargo_env()) + def _install_sc_meta_deps(self): + # this is needed for sc-meta to run the tests + # if os is Windows skip installation + os = platform.system().lower() + if os == "windows": + logger.warning("`sc-meta install all` command is not supported on windows") + return + + logger.info("Installing sc-meta dependencies.") + args = ["sc-meta", "install", "all"] + myprocess.run_process(args) + def _get_installer_url(self) -> str: if workstation.is_windows(): return "https://win.rustup.rs" diff --git a/multiversx_sdk_cli/dns.py b/multiversx_sdk_cli/dns.py index 54485a48..114b1a11 100644 --- a/multiversx_sdk_cli/dns.py +++ b/multiversx_sdk_cli/dns.py @@ -1,12 +1,13 @@ from typing import Any, List, Protocol from Cryptodome.Hash import keccak -from multiversx_sdk_core import Address, AddressComputer +from multiversx_sdk import Address, AddressComputer, TransactionsFactoryConfig +from multiversx_sdk.network_providers.network_config import NetworkConfig from multiversx_sdk_cli import cli_shared, utils from multiversx_sdk_cli.accounts import Account from multiversx_sdk_cli.constants import ADDRESS_ZERO_BECH32, DEFAULT_HRP -from multiversx_sdk_cli.contracts import query_contract +from multiversx_sdk_cli.contracts import SmartContract from multiversx_sdk_cli.transactions import (compute_relayed_v1_data, do_prepare_transaction) @@ -19,22 +20,48 @@ class INetworkProvider(Protocol): def query_contract(self, query: Any) -> Any: ... + def get_network_config(self) -> NetworkConfig: + ... + def resolve(name: str, proxy: INetworkProvider) -> Address: name_arg = "0x{}".format(str.encode(name).hex()) dns_address = dns_address_for_name(name) - result = query_contract(dns_address, proxy, "resolve", [name_arg]) - if len(result) == 0: + response = _query_contract( + contract_address=dns_address, + proxy=proxy, + function="resolve", + args=[name_arg] + ) + + if len(response) == 0: return Address.from_bech32(ADDRESS_ZERO_BECH32) - return Address.from_hex(result[0].hex, DEFAULT_HRP) + + result = response[0].get("returnDataParts")[0] + return Address.from_hex(result, DEFAULT_HRP) def validate_name(name: str, shard_id: int, proxy: INetworkProvider): name_arg = "0x{}".format(str.encode(name).hex()) dns_address = compute_dns_address_for_shard_id(shard_id) - query_contract(dns_address, proxy, "validateName", [name_arg]) + response = _query_contract( + contract_address=dns_address, + proxy=proxy, + function="validateName", + args=[name_arg] + ) + + response = response[0] + + return_code = response["returnCode"] + if return_code == "ok": + print(f"name [{name}] is valid") + else: + print(f"name [{name}] is invalid") + + print(response) def register(args: Any): @@ -69,17 +96,35 @@ def name_hash(name: str) -> bytes: def registration_cost(shard_id: int, proxy: INetworkProvider) -> int: dns_address = compute_dns_address_for_shard_id(shard_id) - result = query_contract(dns_address, proxy, "getRegistrationCost", []) - if len(result[0]) == 0: + + response = _query_contract( + contract_address=dns_address, + proxy=proxy, + function="getRegistrationCost", + args=[] + ) + + response = response[0] + + data = response["returnDataParts"][0] + if not data: return 0 else: - return int("0x{}".format(result[0])) + return int("0x{}".format(data)) def version(shard_id: int, proxy: INetworkProvider) -> str: dns_address = compute_dns_address_for_shard_id(shard_id) - result = query_contract(dns_address, proxy, "version", []) - return bytearray.fromhex(result[0].hex).decode() + + response = _query_contract( + contract_address=dns_address, + proxy=proxy, + function="version", + args=[] + ) + + response = response[0] + return bytearray.fromhex(response["returnDataParts"][0]).decode() def dns_address_for_name(name: str) -> Address: @@ -102,3 +147,17 @@ def compute_dns_address_for_shard_id(shard_id: int) -> Address: def dns_register_data(name: str) -> str: name_enc: bytes = str.encode(name) return "register@{}".format(name_enc.hex()) + + +def _query_contract(contract_address: Address, proxy: INetworkProvider, function: str, args: List[Any]) -> List[Any]: + chain_id = proxy.get_network_config().chain_id + config = TransactionsFactoryConfig(chain_id) + contract = SmartContract(config) + + return contract.query_contract( + contract_address=contract_address, + proxy=proxy, + function=function, + arguments=args, + should_prepare_args=False + ) diff --git a/multiversx_sdk_cli/docker.py b/multiversx_sdk_cli/docker.py index b14e9ef2..02deb8bb 100644 --- a/multiversx_sdk_cli/docker.py +++ b/multiversx_sdk_cli/docker.py @@ -18,7 +18,7 @@ def is_docker_installed(): return True else: return False - except: + except Exception: logger.error("Something went wrong when checking if docker is installed!") diff --git a/multiversx_sdk_cli/errors.py b/multiversx_sdk_cli/errors.py index 13b06e87..b5cb32b4 100644 --- a/multiversx_sdk_cli/errors.py +++ b/multiversx_sdk_cli/errors.py @@ -1,5 +1,3 @@ - -from pathlib import Path from typing import Any, List, Tuple, Union @@ -22,16 +20,6 @@ class ProgrammingError(KnownError): pass -class TemplateMissingError(KnownError): - def __init__(self, template: str): - super().__init__(f"Template missing: {template}") - - -class BadTemplateError(KnownError): - def __init__(self, directory: Path): - super().__init__(f"Bad template: {directory}") - - class DownloadError(KnownError): pass @@ -75,7 +63,7 @@ def __init__(self, directory: str): class BadFile(KnownError): - def __init__(self, filename: str, inner=None): + def __init__(self, filename: str, inner: Any = None): super().__init__(f"Bad file: {filename}.", inner) @@ -104,16 +92,6 @@ def __init__(self, input: str, message: str): super().__init__(f"Bad input [{input}]: {message}") -class BadAddressFormatError(KnownError): - def __init__(self, value: str): - super().__init__(f"Bad address [{value}].") - - -class EmptyAddressError(KnownError): - def __init__(self): - super().__init__("Address is empty.") - - class ExternalProcessError(KnownError): def __init__(self, command_line: str, message: str): super().__init__(f"""External process error: @@ -136,25 +114,6 @@ def __init__(self, name: str): super().__init__(f"This configuration name is protected: {name}.") -class UnsupportedConfigurationValue(KnownError): - pass - - -class UnknownDerivationFunction(KnownError): - def __init__(self): - super().__init__("Unknown key derivation function.") - - -class UnknownCipher(KnownError): - def __init__(self, name: str): - super().__init__(f"Unknown cipher: {name}.") - - -class GasLimitTooLarge(KnownError): - def __init__(self, current: int, limit: int): - super().__init__(f"The gas limit provided ({current}) exceeds the max gas limit of allowed for a transaction ({limit})") - - class BadUserInput(KnownError): def __init__(self, message: str): super().__init__(f"Bad user input: {message}.") @@ -208,3 +167,18 @@ def __init__(self, message: str, url: str, data: str, code: str): "code": code } super().__init__(message, inner) + + +class WalletGenerationError(KnownError): + def __init__(self, message: str): + super().__init__(message) + + +class QueryContractError(KnownError): + def __init__(self, message: str, inner: Any = None): + super().__init__(message, str(inner)) + + +class NativeAuthClientError(KnownError): + def __init__(self, message: str): + super().__init__(message) diff --git a/multiversx_sdk_cli/interfaces.py b/multiversx_sdk_cli/interfaces.py index e59c3c8f..e0db9a2f 100644 --- a/multiversx_sdk_cli/interfaces.py +++ b/multiversx_sdk_cli/interfaces.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Protocol +from typing import Any, Dict, Protocol, Sequence class IAddress(Protocol): @@ -25,6 +25,11 @@ class ITransaction(Protocol): guardian: str signature: bytes guardian_signature: bytes + relayer: str + + @property + def inner_transactions(self) -> Sequence["ITransaction"]: + ... class IAccount(Protocol): diff --git a/multiversx_sdk_cli/ledger/ledger_app_handler.py b/multiversx_sdk_cli/ledger/ledger_app_handler.py index dde814ec..2ec9e955 100644 --- a/multiversx_sdk_cli/ledger/ledger_app_handler.py +++ b/multiversx_sdk_cli/ledger/ledger_app_handler.py @@ -30,7 +30,7 @@ class LedgerApp: def __init__(self): try: self.transport = Transport(interface="hid", debug=False) # Nano S/X using HID interface - except: + except Exception: raise LedgerError(CONNECTION_ERROR_MSG) def close(self): diff --git a/multiversx_sdk_cli/localnet/config_part.py b/multiversx_sdk_cli/localnet/config_part.py index e2df004f..cccdd17f 100644 --- a/multiversx_sdk_cli/localnet/config_part.py +++ b/multiversx_sdk_cli/localnet/config_part.py @@ -24,8 +24,8 @@ def _validate_overriding_entries(self, overriding: Dict[str, Any]) -> None: if unknown_entries: logger.error(f"""\ -Unknown localnet configuration entries: {unknown_entries}. -Please check the configuration of the localnet. +Unknown localnet configuration entries: {unknown_entries}. +Please check the configuration of the localnet. For "{self.get_name()}", the allowed entries are: {allowed_entries}.""") raise UnknownConfigurationError(f"Unknown localnet configuration entries: {unknown_entries}") diff --git a/multiversx_sdk_cli/localnet/wallets.py b/multiversx_sdk_cli/localnet/wallets.py index 2134e889..f4e3e5e2 100644 --- a/multiversx_sdk_cli/localnet/wallets.py +++ b/multiversx_sdk_cli/localnet/wallets.py @@ -3,7 +3,7 @@ from pathlib import Path from typing import Dict, Tuple -from multiversx_sdk_wallet.validator_pem import ValidatorPEM +from multiversx_sdk import ValidatorPEM from multiversx_sdk_cli import errors, utils from multiversx_sdk_cli.accounts import Account diff --git a/multiversx_sdk_cli/native_auth_client.py b/multiversx_sdk_cli/native_auth_client.py new file mode 100644 index 00000000..f7174b8e --- /dev/null +++ b/multiversx_sdk_cli/native_auth_client.py @@ -0,0 +1,104 @@ +import base64 +import json +from typing import Any, Dict, Optional + +import requests + +from multiversx_sdk_cli.errors import NativeAuthClientError + +DEFAULT_EXPIRY_TIME_IN_SECONDS = 60 * 60 * 2 +DEFAULT_API_URL = "https://api.multiversx.com" + + +class NativeAuthClientConfig: + def __init__( + self, + origin: str = '', + api_url: str = DEFAULT_API_URL, + expiry_seconds: int = DEFAULT_EXPIRY_TIME_IN_SECONDS, + block_hash_shard: Optional[int] = None, + gateway_url: Optional[str] = None, + extra_request_headers: Optional[Dict[str, str]] = None + ) -> None: + self.origin = origin + self.api_url = api_url + self.expiry_seconds = expiry_seconds + self.block_hash_shard = block_hash_shard + self.gateway_url = gateway_url + self.extra_request_headers = extra_request_headers + + +class NativeAuthClient: + def __init__(self, config: Optional[NativeAuthClientConfig] = None) -> None: + self.config = config or NativeAuthClientConfig() + + def initialize(self, extra_info: Dict[Any, Any] = {}) -> str: + block_hash = self.get_current_block_hash() + encoded_extra_info = self._encode_value(json.dumps(extra_info)) + encoded_origin = self._encode_value(self.config.origin) + + return f"{encoded_origin}.{block_hash}.{self.config.expiry_seconds}.{encoded_extra_info}" + + def get_token(self, address: str, token: str, signature: str) -> str: + encoded_address = self._encode_value(address) + encoded_token = self._encode_value(token) + + return f"{encoded_address}.{encoded_token}.{signature}" + + def get_current_block_hash(self) -> str: + if self.config.gateway_url: + return self._get_current_block_hash_using_gateway() + return self._get_current_block_hash_using_api() + + def _get_current_block_hash_using_gateway(self) -> str: + round = self._get_current_round() + url = f"{self.config.gateway_url}/blocks/by-round/{round}" + response = self._execute_request(url) + blocks = response["data"]["blocks"] + block = [b for b in blocks if b["shard"] == self.config.block_hash_shard][0] + return block["hash"] + + def _get_current_round(self) -> int: + if self.config.gateway_url is None: + raise NativeAuthClientError("Gateway URL not set") + + if self.config.block_hash_shard is None: + raise NativeAuthClientError("Blockhash shard not set") + + url = f"{self.config.gateway_url}/network/status/{self.config.block_hash_shard}" + response = self._execute_request(url) + status = response["data"]["status"] + + return status["erd_current_round"] + + def _get_current_block_hash_using_api(self) -> str: + try: + url = f"{self.config.api_url}/blocks/latest?ttl={self.config.expiry_seconds}&fields=hash" + response = self._execute_request(url) + if response["hash"]: + return response["hash"] + except Exception: + pass + + return self._get_current_block_hash_using_api_fallback() + + def _get_current_block_hash_using_api_fallback(self) -> str: + url = f"{self.config.api_url}/blocks?size=1&fields=hash" + + if self.config.block_hash_shard: + url += f"&shard={self.config.block_hash_shard}" + + response = self._execute_request(url) + return response[0]["hash"] + + def _encode_value(self, string: str) -> str: + encoded = base64.b64encode(string.encode('utf-8')).decode('utf-8') + return self._escape(encoded) + + def _escape(self, string: str) -> str: + return string.replace("+", "-").replace("/", "_").replace("=", "") + + def _execute_request(self, url: str) -> Any: + response = requests.get(url=url, headers=self.config.extra_request_headers) + response.raise_for_status() + return response.json() diff --git a/multiversx_sdk_cli/projects/core.py b/multiversx_sdk_cli/projects/core.py index ebba16e6..1a663731 100644 --- a/multiversx_sdk_cli/projects/core.py +++ b/multiversx_sdk_cli/projects/core.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Any, List -from multiversx_sdk_cli import dependencies, errors, guards +from multiversx_sdk_cli import errors, guards from multiversx_sdk_cli.projects import shared from multiversx_sdk_cli.projects.constants import (OLD_PROJECT_CONFIG_FILENAME, PROJECT_CONFIG_FILENAME) @@ -42,17 +42,14 @@ def clean_project(directory: Path): logger.info("Project cleaned.") -def run_tests(project_path: Path, args: Any): - directory = Path(args.directory) - wildcard = args.wildcard +def run_tests(args: Any): + directory = Path(args.path) - logger.info("run_tests.project: %s", project_path) + logger.info("run_tests.project: %s", directory) - dependencies.install_module("vmtools") - - guards.is_directory(project_path) - project = load_project(project_path) - project.run_tests(directory, wildcard) + guards.is_directory(directory) + project = load_project(directory) + project.run_tests(args) def get_project_paths_recursively(base_path: Path) -> List[Path]: diff --git a/multiversx_sdk_cli/projects/project_base.py b/multiversx_sdk_cli/projects/project_base.py index 74777af1..75d7db8b 100644 --- a/multiversx_sdk_cli/projects/project_base.py +++ b/multiversx_sdk_cli/projects/project_base.py @@ -1,13 +1,11 @@ -import glob import logging import shutil from abc import abstractmethod from os import path from pathlib import Path -from typing import Any, Dict, List, Union, cast, final +from typing import Any, Dict, List, Union, final from multiversx_sdk_cli import dependencies, errors, myprocess, utils -from multiversx_sdk_cli.dependencies.modules import StandaloneModule from multiversx_sdk_cli.projects.constants import PROJECT_CONFIG_FILENAME from multiversx_sdk_cli.projects.interfaces import IProject from multiversx_sdk_cli.projects.migrations import migrate_project_config_file @@ -129,23 +127,22 @@ def ensure_config_file(self) -> None: def default_config(self) -> Dict[str, Any]: return dict() - def run_tests(self, tests_directory: Path, wildcard: str = ""): - vmtools = cast(StandaloneModule, dependencies.get_module_by_key("vmtools")) - tool_env = vmtools.get_env() - tool = path.join(vmtools.get_parent_directory(), "run-scenarios") - test_folder = self.directory / tests_directory - - if not wildcard: - args = [tool, str(test_folder)] - myprocess.run_process(args, env=tool_env) - else: - pattern = test_folder / wildcard - test_files = glob.glob(str(pattern)) - - for test_file in test_files: - print("Run test for:", test_file) - args = [tool, test_file] - myprocess.run_process(args, env=tool_env) + def run_tests(self, args: Any): + command = ["sc-meta", "test"] + + if args.path: + command.extend(["--path", str(Path(args.path).expanduser())]) + + if args.go: + command.append("--go") + + if args.scen: + command.append("--scen") + + if args.nocapture: + command.append("--nocapture") + + myprocess.run_process(command) def glob_files(folder: Path, pattern: str) -> List[Path]: diff --git a/multiversx_sdk_cli/projects/report/data/folder_report.py b/multiversx_sdk_cli/projects/report/data/folder_report.py index 31c78064..d0559379 100644 --- a/multiversx_sdk_cli/projects/report/data/folder_report.py +++ b/multiversx_sdk_cli/projects/report/data/folder_report.py @@ -1,9 +1,12 @@ from pathlib import Path from typing import Any, List, Optional -from multiversx_sdk_cli.projects.report.data.common import first_not_none, flatten_list_of_rows, merge_values_by_key -from multiversx_sdk_cli.projects.report.data.project_report import ProjectReport, merge_list_of_projects -from multiversx_sdk_cli.projects.report.format.format_options import FormatOptions +from multiversx_sdk_cli.projects.report.data.common import ( + first_not_none, flatten_list_of_rows, merge_values_by_key) +from multiversx_sdk_cli.projects.report.data.project_report import ( + ProjectReport, merge_list_of_projects) +from multiversx_sdk_cli.projects.report.format.format_options import \ + FormatOptions class FolderReport: @@ -17,6 +20,7 @@ def to_json(self) -> Any: 'projects': self.projects } + @staticmethod def from_json(json: Any) -> 'FolderReport': projects = [ProjectReport.from_json(project) for project in json['projects']] return FolderReport(Path(json['root_path']), projects) diff --git a/multiversx_sdk_cli/sign_verify.py b/multiversx_sdk_cli/sign_verify.py index 240a843e..09b45a55 100644 --- a/multiversx_sdk_cli/sign_verify.py +++ b/multiversx_sdk_cli/sign_verify.py @@ -1,7 +1,6 @@ from typing import Dict -from multiversx_sdk_core import Address, Message, MessageComputer -from multiversx_sdk_wallet import UserVerifier +from multiversx_sdk import Address, Message, MessageComputer, UserVerifier from multiversx_sdk_cli.accounts import Account diff --git a/multiversx_sdk_cli/simulation.py b/multiversx_sdk_cli/simulation.py index a6b1bbd4..bd5c41c6 100644 --- a/multiversx_sdk_cli/simulation.py +++ b/multiversx_sdk_cli/simulation.py @@ -1,5 +1,6 @@ from collections import OrderedDict from typing import Any, Dict, Protocol + from multiversx_sdk_cli.interfaces import ISimulateResponse, ITransaction from multiversx_sdk_cli.utils import ISerializable @@ -19,6 +20,7 @@ def to_dictionary(self) -> Dict[str, Any]: return dictionary + class Simulator(): def __init__(self, proxy: INetworkProvider) -> None: self.proxy = proxy diff --git a/multiversx_sdk_cli/tests/local_verify_server.py b/multiversx_sdk_cli/tests/local_verify_server.py index 010177c3..dbc989ab 100644 --- a/multiversx_sdk_cli/tests/local_verify_server.py +++ b/multiversx_sdk_cli/tests/local_verify_server.py @@ -1,5 +1,5 @@ -from http.server import HTTPServer, BaseHTTPRequestHandler import json +from http.server import BaseHTTPRequestHandler, HTTPServer HOST = 'localhost' PORT = 7777 @@ -14,7 +14,7 @@ def do_POST(self): if self.path == "/initialise": response = {'token': 7890} self.wfile.write(bytes(json.dumps(response), 'utf-8')) - + if self.path == "/verify": response = {'status': 'sent to verification'} self.wfile.write(bytes(json.dumps(response), 'utf-8')) diff --git a/multiversx_sdk_cli/tests/test_cli_contracts.py b/multiversx_sdk_cli/tests/test_cli_contracts.py index b69b9867..12e4c523 100644 --- a/multiversx_sdk_cli/tests/test_cli_contracts.py +++ b/multiversx_sdk_cli/tests/test_cli_contracts.py @@ -107,7 +107,7 @@ def test_contract_deploy(): str(output_file), ] ) - assert Path.is_file(output_file) == True + assert Path.is_file(output_file) def test_contract_upgrade(): @@ -136,7 +136,7 @@ def test_contract_upgrade(): str(output_file), ] ) - assert Path.is_file(output_file) == True + assert Path.is_file(output_file) def test_contract_call(): @@ -165,7 +165,7 @@ def test_contract_call(): str(output_file), ] ) - assert Path.is_file(output_file) == True + assert Path.is_file(output_file) def test_contract_transfer_and_execute(capsys: Any): @@ -223,20 +223,19 @@ def test_contract_flow(capsys: Any): capsys.readouterr() main([ - "contract", "query", - contract, + "contract", "query", contract, "--function", "getSum", "--proxy", "https://testnet-api.multiversx.com" ]) response = get_query_response(capsys) - assert response == "" + assert len(response) == 1 + assert response == [""] # Clear the captured content capsys.readouterr() main([ - "contract", "call", - contract, + "contract", "call", contract, "--pem", alice, "--function", "add", "--recall-nonce", @@ -250,20 +249,19 @@ def test_contract_flow(capsys: Any): capsys.readouterr() main([ - "contract", "query", - contract, + "contract", "query", contract, "--function", "getSum", "--proxy", "https://testnet-api.multiversx.com" ]) response = get_query_response(capsys) - assert response["number"] == 7 + assert len(response) == 1 + assert response == ["07"] # Clear the captured content capsys.readouterr() main([ - "contract", "upgrade", - contract, + "contract", "upgrade", contract, "--bytecode", adder, "--pem", alice, "--recall-nonce", @@ -317,6 +315,179 @@ def test_contract_commands_argument_parameter(): assert not return_code +def test_contract_deploy_with_abi(capsys: Any): + alice = f"{parent}/testdata/alice.pem" + multisig = f"{parent}/testdata/multisig.wasm" + multisig_abi = f"{parent}/testdata/multisig.abi.json" + + return_code = main([ + "contract", "deploy", + "--bytecode", multisig, + "--pem", alice, + "--chain", "T", + "--nonce", "7", + "--gas-limit", "5000000", + "--arguments", "2", "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + ]) + assert not return_code + + deploy_without_abi_data = get_transaction_data(capsys) + # Clear the captured content + capsys.readouterr() + + return_code = main([ + "contract", "deploy", + "--bytecode", multisig, + "--abi", multisig_abi, + "--pem", alice, + "--chain", "T", + "--nonce", "7", + "--gas-limit", "5000000", + "--arguments-file", f"{parent}/testdata/deploy_multisig_args.json" + ]) + assert not return_code + + deploy_with_abi_data = get_transaction_data(capsys) + assert deploy_without_abi_data == deploy_with_abi_data + assert deploy_without_abi_data.endswith("@02@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1@c0006edaaee4fd479f2f248b341eb11eaecaec4d7dee190619958332bba5200f") + assert deploy_with_abi_data.endswith("@02@0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1@c0006edaaee4fd479f2f248b341eb11eaecaec4d7dee190619958332bba5200f") + + +def test_contract_call_with_abi(capsys: Any): + alice = f"{parent}/testdata/alice.pem" + multisig_abi = f"{parent}/testdata/multisig.abi.json" + + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgqpg2tnjelx3s3dhkksqhjavprtgmt9xt4d8ss9al8m4", + "--pem", alice, + "--chain", "T", + "--nonce", "7", + "--gas-limit", "5000000", + "--function", "proposeBatch", + "--abi", multisig_abi, + "--arguments-file", f"{parent}/testdata/call_multisig_propose_batch_args.json" + ]) + assert not return_code + + data = get_transaction_data(capsys) + assert data == "proposeBatch@0500000000000000000500ed8e25a94efa837aae0e593112cfbb01b448755069e1000000080de0b6b3a7640000010000000000e4e1c000000003616464000000010000000107" + + +def test_contract_upgrade_with_abi(capsys: Any): + alice = f"{parent}/testdata/alice.pem" + multisig_abi = f"{parent}/testdata/multisig.abi.json" + + return_code = main([ + "contract", "call", "erd1qqqqqqqqqqqqqpgqpg2tnjelx3s3dhkksqhjavprtgmt9xt4d8ss9al8m4", + "--pem", alice, + "--chain", "T", + "--nonce", "7", + "--gas-limit", "5000000", + "--function", "proposeSCUpgradeFromSource", + "--abi", multisig_abi, + "--arguments-file", f"{parent}/testdata/upgrade_multisig_args.json" + ]) + assert not return_code + + data = get_transaction_data(capsys) + assert data == "proposeSCUpgradeFromSource@000000000000000005000a14b9cb3f346116ded6802f2eb0235a36b2997569e1@@00000000000000000500ed8e25a94efa837aae0e593112cfbb01b448755069e1@0500@" + + +def test_contract_query(capsys: Any): + alice = f"{parent}/testdata/alice.pem" + adder = f"{parent}/testdata/adder.wasm" + adder_abi = f"{parent}/testdata/adder.abi.json" + + return_code = main([ + "contract", "deploy", + "--bytecode", adder, + "--abi", adder_abi, + "--pem", alice, + "--recall-nonce", + "--gas-limit", "5000000", + "--proxy", "https://testnet-api.multiversx.com", + "--arguments", "0", + "--send", "--wait-result" + ]) + assert not return_code + contract = get_contract_address(capsys) + + # Clear the captured content + capsys.readouterr() + + return_code = main([ + "contract", "call", contract, + "--pem", alice, + "--function", "add", + "--recall-nonce", + "--gas-limit", "5000000", + "--proxy", "https://testnet-api.multiversx.com", + "--arguments", "14", + "--send", "--wait-result" + ]) + assert not return_code + + # Clear the captured content + capsys.readouterr() + + # invalid, without abi + return_code = main([ + "contract", "query", contract, + "--function", "getSummm", + "--proxy", "https://testnet-api.multiversx.com" + ]) + assert return_code + output = _read_stdout(capsys) + if "invalid function (not found)" in output: + assert True + else: + assert False + + # Clear the captured content + capsys.readouterr() + + # invalid, with abi, error is thrown by sdk-py + return_code = main([ + "contract", "query", contract, + "--function", "getSummm", + "--abi", adder_abi, + "--proxy", "https://testnet-api.multiversx.com" + ]) + assert return_code + output = _read_stdout(capsys) + if "endpoint 'getSummm' not found" in output: + assert True + else: + assert False + + # Clear the captured content + capsys.readouterr() + + # query contract, without abi + return_code = main([ + "contract", "query", contract, + "--function", "getSum", + "--proxy", "https://testnet-api.multiversx.com" + ]) + assert not return_code + response = get_query_response(capsys) + assert response == ["0e"] + + # Clear the captured content + capsys.readouterr() + + # query contract, without abi + return_code = main([ + "contract", "query", contract, + "--function", "getSum", + "--abi", adder_abi, + "--proxy", "https://testnet-api.multiversx.com" + ]) + assert not return_code + response = get_query_response(capsys) + assert response == [14] + + def _read_stdout(capsys: Any) -> str: return capsys.readouterr().out.strip() @@ -329,10 +500,10 @@ def get_contract_address(capsys: Any): def get_query_response(capsys: Any): out = _read_stdout(capsys).replace("\n", "").replace(" ", "") - return json.loads(out)[0] + return json.loads(out) -def get_transaction_data(capsys: Any): +def get_transaction_data(capsys: Any) -> str: out = _read_stdout(capsys) output = json.loads(out) return output["emittedTransactionData"] diff --git a/multiversx_sdk_cli/tests/test_cli_contracts.sh b/multiversx_sdk_cli/tests/test_cli_contracts.sh index fe980cfe..bf7f9fa6 100755 --- a/multiversx_sdk_cli/tests/test_cli_contracts.sh +++ b/multiversx_sdk_cli/tests/test_cli_contracts.sh @@ -36,8 +36,8 @@ testBuildContracts() { testRunScenarios() { echo "testRunScenarios" - ${CLI} --verbose contract test --directory="scenarios" ${SANDBOX}/adder || return 1 - ${CLI} --verbose contract test --directory="scenarios" ${SANDBOX}/empty || return 1 + ${CLI} --verbose contract test --path=${SANDBOX}/adder || return 1 + ${CLI} --verbose contract test --path=${SANDBOX}/empty || return 1 } testWasmName() { diff --git a/multiversx_sdk_cli/tests/test_cli_deps.py b/multiversx_sdk_cli/tests/test_cli_deps.py index 762e3d24..63162d71 100644 --- a/multiversx_sdk_cli/tests/test_cli_deps.py +++ b/multiversx_sdk_cli/tests/test_cli_deps.py @@ -31,18 +31,6 @@ def test_deps_check_rust(): assert which_twiggy and Path.is_file(Path(which_twiggy)) -@pytest.mark.skip_on_windows -def test_deps_install_vmtools(): - return_code = main(["deps", "install", "vmtools"]) - assert return_code == 0 - - -@pytest.mark.skip_on_windows -def test_deps_check_vmtools(): - return_code = main(["deps", "check", "vmtools"]) - assert return_code == 0 - - def test_deps_install_testwallets(): return_code = main(["deps", "install", "testwallets"]) assert return_code == 0 diff --git a/multiversx_sdk_cli/tests/test_cli_shared.py b/multiversx_sdk_cli/tests/test_cli_shared.py index 04944866..4a1b1107 100644 --- a/multiversx_sdk_cli/tests/test_cli_shared.py +++ b/multiversx_sdk_cli/tests/test_cli_shared.py @@ -40,7 +40,7 @@ def test_args_obj_to_list(): assert args_list[1] == contract_build_args.path assert args_list[2] == "--no-wasm-opt" - contract_build_args.ignore = "random_directory" + contract_build_args.ignore = "random_directory" # type: ignore contract_build_args.no_imports = True args_list = convert_args_object_to_args_list(contract_build_args) diff --git a/multiversx_sdk_cli/tests/test_cli_staking_provider.py b/multiversx_sdk_cli/tests/test_cli_staking_provider.py index 7c141820..a657c0d1 100644 --- a/multiversx_sdk_cli/tests/test_cli_staking_provider.py +++ b/multiversx_sdk_cli/tests/test_cli_staking_provider.py @@ -74,11 +74,11 @@ def test_add_nodes(capsys: Any): data = tx["emittedTransactionData"] transaction = tx["emittedTransaction"] - assert data == "addNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208@604882237a9845f508ad03877b5aab90569683eeb51fafcbbeb87440ba359992b3c0b837a8757c25be18132549404f88@78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d@ec54a009695af56c3585ef623387b67b6df1974b0b3c9138eb64bde6eb33978ae9851112b20c99bf63588e8e949e4388@7188b234a8bf834f2e6258012aa09a2ab93178ffab9c789480275f61fe02cd1b9a58ddc63b79a73abea9e2b7ac5cac0b0d4324eff50aca2f0ec946b9ae6797511fa3ce461b57e77129cba8ab3b51147695d4ce889cbe67905f6586b4e4f22491@c6c637de17db5f89a2fa1d1d935cb60c0e5e8958d3bfc47f903f774dd97398c8fe22093e113865ee98c3afdd1de62694" + assert data == "addNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208@307ef00b648eed52ce4e95f8155f2e65491addd49266254f7c32ff06622e335752a6a6ad2efeb2534ec0db6a431b4989@78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d@4c90003d4b535fe709b6583708ae276e29558c58d160ad6241c3063e590611a2e327f2b8299b4955179a85eab50b4587@7188b234a8bf834f2e6258012aa09a2ab93178ffab9c789480275f61fe02cd1b9a58ddc63b79a73abea9e2b7ac5cac0b0d4324eff50aca2f0ec946b9ae6797511fa3ce461b57e77129cba8ab3b51147695d4ce889cbe67905f6586b4e4f22491@1d6cf6b0a38fa5c10df6493eae0b14c5d119d9f7d3d3e790a047e4e7c09919f64f910971ceef380b49f70fc982d07d19" assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh" assert transaction["gasLimit"] == 20367000 - assert transaction["signature"] == "b383909206bf9631d5bef583c6e28250815494b459977fe8f037c2a97d2692a77d1b5c5dda6095d64ad180d213b5fd5eb7038a54af3765a3cb3fd86b86a1f305" + assert transaction["signature"] == "a0a8ce8ee6c60bd44b3940e47f3598fc95b47d111cf3ce7752951c692f98ccc8e0a2eade3ac55fcc68c54f98c69a0fb7aa0c93a50e8a8bcd0206f84a95e6390a" def test_add_nodes_with_gas_limit(capsys: Any): @@ -97,11 +97,11 @@ def test_add_nodes_with_gas_limit(capsys: Any): data = tx["emittedTransactionData"] transaction = tx["emittedTransaction"] - assert data == "addNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208@604882237a9845f508ad03877b5aab90569683eeb51fafcbbeb87440ba359992b3c0b837a8757c25be18132549404f88@78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d@ec54a009695af56c3585ef623387b67b6df1974b0b3c9138eb64bde6eb33978ae9851112b20c99bf63588e8e949e4388@7188b234a8bf834f2e6258012aa09a2ab93178ffab9c789480275f61fe02cd1b9a58ddc63b79a73abea9e2b7ac5cac0b0d4324eff50aca2f0ec946b9ae6797511fa3ce461b57e77129cba8ab3b51147695d4ce889cbe67905f6586b4e4f22491@c6c637de17db5f89a2fa1d1d935cb60c0e5e8958d3bfc47f903f774dd97398c8fe22093e113865ee98c3afdd1de62694" + assert data == "addNodes@e7beaa95b3877f47348df4dd1cb578a4f7cabf7a20bfeefe5cdd263878ff132b765e04fef6f40c93512b666c47ed7719b8902f6c922c04247989b7137e837cc81a62e54712471c97a2ddab75aa9c2f58f813ed4c0fa722bde0ab718bff382208@307ef00b648eed52ce4e95f8155f2e65491addd49266254f7c32ff06622e335752a6a6ad2efeb2534ec0db6a431b4989@78689fd4b1e2e434d567fe01e61598a42717d83124308266bd09ccc15d2339dd318c019914b86ac29adbae5dd8a02d0307425e9bd85a296e94943708c72f8c670f0b7c50a890a5719088dbd9f1d062cad9acffa06df834106eebe1a4257ef00d@4c90003d4b535fe709b6583708ae276e29558c58d160ad6241c3063e590611a2e327f2b8299b4955179a85eab50b4587@7188b234a8bf834f2e6258012aa09a2ab93178ffab9c789480275f61fe02cd1b9a58ddc63b79a73abea9e2b7ac5cac0b0d4324eff50aca2f0ec946b9ae6797511fa3ce461b57e77129cba8ab3b51147695d4ce889cbe67905f6586b4e4f22491@1d6cf6b0a38fa5c10df6493eae0b14c5d119d9f7d3d3e790a047e4e7c09919f64f910971ceef380b49f70fc982d07d19" assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh" assert transaction["gasLimit"] == 20367001 - assert transaction["signature"] == "a889b50844eb5b33d410cbc8c8d2d88eebd64839d22fc1d246b82315123bd3acc6ad4445f9d2430965ce516ff89256faea42a97ebba2e1b386147ff8328b2e01" + assert transaction["signature"] == "b4aa91a3c865cf82784f29a659c0884cb70948e0e3f0b74ec8fe5a3cce358e750c357f1a1f9378e855a4261fff10abd927d1e3535fd18e99c40a562e31d67406" def test_remove_nodes_with_bls_keys(capsys: Any): @@ -223,6 +223,7 @@ def test_unjail_nodes(capsys: Any): "staking-provider", "unjail-nodes", "--bls-keys", f"{first_bls_key},{second_bls_key}", "--delegation-contract", "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh", + "--value", "5000000000000000000", "--pem", str(alice), "--chain", "T", "--nonce", "7", "--estimate-gas" @@ -235,6 +236,7 @@ def test_unjail_nodes(capsys: Any): assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh" assert transaction["gasLimit"] == 13645500 + assert transaction["value"] == "5000000000000000000" def test_change_service_fee(capsys: Any): @@ -374,6 +376,117 @@ def test_set_metadata(capsys: Any): assert transaction["gasLimit"] == 11131000 +def test_create_delegation_contract_from_validator(capsys: Any): + main([ + "staking-provider", "make-delegation-contract-from-validator", + "--max-cap", "0", + "--fee", "3745", + "--pem", str(alice), + "--nonce", "7", "--estimate-gas", + "--chain", "T" + ]) + tx = get_transaction(capsys) + data = tx["emittedTransactionData"] + transaction = tx["emittedTransaction"] + + assert data == "makeNewContractFromValidatorData@@0ea1" + assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqylllslmq6y6" + assert transaction["gasLimit"] == 510000000 + + +def test_delegate(capsys: Any): + main([ + "staking-provider", "delegate", + "--delegation-contract", "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh", + "--value", "1000000000000000000", + "--pem", str(alice), + "--nonce", "7", "--estimate-gas", + "--chain", "T" + ]) + tx = get_transaction(capsys) + data = tx["emittedTransactionData"] + transaction = tx["emittedTransaction"] + + assert data == "delegate" + assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh" + assert transaction["gasLimit"] == 12000000 + + +def test_claim_rewards(capsys: Any): + main([ + "staking-provider", "claim-rewards", + "--delegation-contract", "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh", + "--pem", str(alice), + "--nonce", "7", "--estimate-gas", + "--chain", "T" + ]) + tx = get_transaction(capsys) + data = tx["emittedTransactionData"] + transaction = tx["emittedTransaction"] + + assert data == "claimRewards" + assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh" + assert transaction["gasLimit"] == 6000000 + + +def test_redelegate_rewards(capsys: Any): + main([ + "staking-provider", "redelegate-rewards", + "--delegation-contract", "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh", + "--pem", str(alice), + "--nonce", "7", "--estimate-gas", + "--chain", "T" + ]) + tx = get_transaction(capsys) + data = tx["emittedTransactionData"] + transaction = tx["emittedTransaction"] + + assert data == "reDelegateRewards" + assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh" + assert transaction["gasLimit"] == 12000000 + + +def test_undelegate(capsys: Any): + main([ + "staking-provider", "undelegate", + "--delegation-contract", "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh", + "--value", "1000000000000000000", + "--pem", str(alice), + "--nonce", "7", "--estimate-gas", + "--chain", "T" + ]) + tx = get_transaction(capsys) + data = tx["emittedTransactionData"] + transaction = tx["emittedTransaction"] + + assert data == "unDelegate@0de0b6b3a7640000" + assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh" + assert transaction["gasLimit"] == 12000000 + + +def test_withdraw(capsys: Any): + main([ + "staking-provider", "withdraw", + "--delegation-contract", "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh", + "--pem", str(alice), + "--nonce", "7", "--estimate-gas", + "--chain", "T" + ]) + tx = get_transaction(capsys) + data = tx["emittedTransactionData"] + transaction = tx["emittedTransaction"] + + assert data == "withdraw" + assert transaction["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert transaction["receiver"] == "erd1qqqqqqqqqqqqqqqpqqqqqqqqqqqqqqqqqqqqqqqqqqqqqthllllsy5r6rh" + assert transaction["gasLimit"] == 12000000 + + def _read_stdout(capsys: Any) -> str: return capsys.readouterr().out.strip() diff --git a/multiversx_sdk_cli/tests/test_cli_transactions.py b/multiversx_sdk_cli/tests/test_cli_transactions.py index 35897304..c8148ae3 100644 --- a/multiversx_sdk_cli/tests/test_cli_transactions.py +++ b/multiversx_sdk_cli/tests/test_cli_transactions.py @@ -1,10 +1,12 @@ import json +import os from pathlib import Path -from typing import Any +from typing import Any, List from multiversx_sdk_cli.cli import main testdata_path = Path(__file__).parent / "testdata" +testdata_out = Path(__file__).parent / "testdata-out" def test_relayed_v1_transaction(capsys: Any): @@ -23,7 +25,7 @@ def test_relayed_v1_transaction(capsys: Any): "--chain", "T", "--relay" ]) - assert False if return_code else True + assert return_code == 0 relayed_tx = _read_stdout(capsys) assert relayed_tx == "relayedTx@7b226e6f6e6365223a3139382c2273656e646572223a2267456e574f65576d6d413063306a6b71764d354241707a61644b46574e534f69417643575163776d4750673d222c227265636569766572223a22414141414141414141414141415141414141414141414141414141414141414141414141414141432f2f383d222c2276616c7565223a302c226761735072696365223a313030303030303030302c226761734c696d6974223a36303030303030302c2264617461223a225a3256305132397564484a68593352446232356d6157633d222c227369676e6174757265223a2239682b6e6742584f5536776674315464437368534d4b3454446a5a32794f74686336564c576e3478724d5a706248427738677a6c6659596d362b766b505258303764634a562b4745635462616a7049692b5a5a5942773d3d222c22636861696e4944223a2256413d3d222c2276657273696f6e223a317d" @@ -40,7 +42,7 @@ def test_create_tx_and_sign_by_hash(capsys: Any): "--options", "1", "--chain", "integration tests chain ID", ]) - assert False if return_code else True + assert return_code == 0 tx = _read_stdout(capsys) tx_json = json.loads(tx) @@ -48,5 +50,132 @@ def test_create_tx_and_sign_by_hash(capsys: Any): assert signature == "f0c81f2393b1ec5972c813f817bae8daa00ade91c6f75ea604ab6a4d2797aca4378d783023ff98f1a02717fe4f24240cdfba0b674ee9abb18042203d713bc70a" +def test_create_move_balance_transaction(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "alice.pem"), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "215", + "--gas-limit", "500000", + "--value", "1000000000000", + "--data", "hello", + "--version", "2", + "--options", "0", + "--chain", "T", + ]) + assert return_code == 0 + tx = _read_stdout(capsys) + tx_json = json.loads(tx) + signature = tx_json["emittedTransaction"]["signature"] + assert signature == "e88d846800bab1751e222c4461a310a3882312ef6d75fd8b861a2f3b572837b58f146ff9d60d16e617f53358d6cfa87cbcc65ad624c77003779d474059264901" + + +def test_create_multi_transfer_transaction(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "alice.pem"), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "212", + "--gas-limit", "5000000", + "--token-transfers", "SSSSS-941b91-01", "1", "TEST-738c3d", "1200000000", + "--version", "2", + "--options", "0", + "--chain", "T", + ]) + assert return_code == 0 + tx = _read_stdout(capsys) + tx_json = json.loads(tx) + signature = tx_json["emittedTransaction"]["signature"] + assert signature == "575b029d52ff5ffbfb7bab2f04052de88a6f7d022a6ad368459b8af9acaed3717d3f95db09f460649a8f405800838bc2c432496bd03c9039ea166bd32b84660e" + + +def test_create_and_save_inner_transaction(): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "alice.pem"), + "--receiver", "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx", + "--nonce", "77", + "--gas-limit", "500000", + "--relayer", "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + "--inner-transactions-outfile", str(testdata_out / "inner_transactions.json"), + "--chain", "T", + ]) + assert False if return_code else True + assert Path(testdata_out / "inner_transactions.json").is_file() + + +def test_create_and_append_inner_transaction(): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "alice.pem"), + "--receiver", "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan", + "--nonce", "1234", + "--gas-limit", "50000", + "--relayer", "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8", + "--inner-transactions-outfile", str(testdata_out / "inner_transactions.json"), + "--chain", "T", + ]) + assert False if return_code else True + + with open(testdata_out / "inner_transactions.json", "r") as file: + json_file = json.load(file) + + inner_txs: List[Any] = json_file["innerTransactions"] + assert len(inner_txs) == 2 + + +def test_create_invalid_relayed_transaction(): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "testUser.pem"), + "--receiver", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", + "--nonce", "987", + "--gas-limit", "5000000", + "--inner-transactions", str(testdata_out / "inner_transactions.json"), + "--data", "test data", + "--chain", "T", + ]) + assert return_code + + +def test_create_relayer_transaction(capsys: Any): + return_code = main([ + "tx", "new", + "--pem", str(testdata_path / "testUser.pem"), + "--receiver", "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5", + "--nonce", "987", + "--gas-limit", "5000000", + "--inner-transactions", str(testdata_out / "inner_transactions.json"), + "--chain", "T", + ]) + # remove test file to ensure consistency when running test file locally + os.remove(testdata_out / "inner_transactions.json") + + assert False if return_code else True + + tx = _read_stdout(capsys) + tx_json = json.loads(tx)["emittedTransaction"] + + assert tx_json["sender"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + assert tx_json["receiver"] == "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + assert tx_json["gasLimit"] == 5000000 + assert tx_json["nonce"] == 987 + assert tx_json["chainID"] == "T" + + # should be the two inner transactions created in the tests above + inner_transactions = tx_json["innerTransactions"] + assert len(inner_transactions) == 2 + + assert inner_transactions[0]["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert inner_transactions[0]["receiver"] == "erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx" + assert inner_transactions[0]["nonce"] == 77 + assert inner_transactions[0]["relayer"] == "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8" + + assert inner_transactions[1]["sender"] == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + assert inner_transactions[1]["receiver"] == "erd1fggp5ru0jhcjrp5rjqyqrnvhr3sz3v2e0fm3ktknvlg7mcyan54qzccnan" + assert inner_transactions[1]["nonce"] == 1234 + assert inner_transactions[1]["relayer"] == "erd1k2s324ww2g0yj38qn2ch2jwctdy8mnfxep94q9arncc6xecg3xaq6mjse8" + + def _read_stdout(capsys: Any) -> str: return capsys.readouterr().out.strip() diff --git a/multiversx_sdk_cli/tests/test_cli_wallet.py b/multiversx_sdk_cli/tests/test_cli_wallet.py index 5255b4d3..6121dbcc 100644 --- a/multiversx_sdk_cli/tests/test_cli_wallet.py +++ b/multiversx_sdk_cli/tests/test_cli_wallet.py @@ -3,7 +3,8 @@ from pathlib import Path from typing import Any -from multiversx_sdk_wallet import Mnemonic, UserPEM, UserWallet +from multiversx_sdk import (Address, AddressComputer, Mnemonic, UserPEM, + UserWallet) from multiversx_sdk_cli.cli import main @@ -17,6 +18,22 @@ def test_wallet_new(capsys: Any): assert Mnemonic.is_text_valid(displayed_mnemonic) +def test_generate_wallet_in_specific_shard(capsys: Any): + address_computer = AddressComputer() + + main(["wallet", "new", "--shard", "0"]) + address = Address.new_from_bech32(_read_stdout_wallet_address(capsys)) + assert address_computer.get_shard_of_address(address) == 0 + + main(["wallet", "new", "--shard", "1"]) + address = Address.new_from_bech32(_read_stdout_wallet_address(capsys)) + assert address_computer.get_shard_of_address(address) == 1 + + main(["wallet", "new", "--shard", "2"]) + address = Address.new_from_bech32(_read_stdout_wallet_address(capsys)) + assert address_computer.get_shard_of_address(address) == 2 + + def test_wallet_new_and_save_in_pem_format(capsys: Any): outfile = testdata_out_path / "testWallet.pem" outfile.unlink(missing_ok=True) @@ -371,9 +388,14 @@ def _read_stdout_mnemonic(capsys: Any) -> str: return lines[0].replace("Mnemonic:", "").strip() +def _read_stdout_wallet_address(capsys: Any) -> str: + lines = _read_stdout(capsys).split("\n") + return lines[1].replace("Wallet address:", "").strip() + + def _read_stdout(capsys: Any) -> str: return capsys.readouterr().out.strip() def _mock_getpass(monkeypatch: Any, password: str): - monkeypatch.setattr(getpass, "getpass", lambda _: password) + monkeypatch.setattr(getpass, "getpass", lambda _: password) # type: ignore diff --git a/multiversx_sdk_cli/tests/test_contracts.py b/multiversx_sdk_cli/tests/test_contracts.py index 733969a3..38f214ce 100644 --- a/multiversx_sdk_cli/tests/test_contracts.py +++ b/multiversx_sdk_cli/tests/test_contracts.py @@ -3,14 +3,12 @@ import pytest from Cryptodome.Hash import keccak -from multiversx_sdk_core.address import Address +from multiversx_sdk import Address, TransactionsFactoryConfig from multiversx_sdk_cli import errors from multiversx_sdk_cli.accounts import Account from multiversx_sdk_cli.contract_verification import _create_request_signature -from multiversx_sdk_cli.contracts import (_interpret_as_number_if_safely, - _prepare_argument, - prepare_args_for_factory) +from multiversx_sdk_cli.contracts import SmartContract, _prepare_argument logging.basicConfig(level=logging.INFO) @@ -63,23 +61,18 @@ def test_contract_verification_create_request_signature(): assert signature.hex() == "30111258cc42ea08e0c6a3e053cc7086a88d614b8b119a244904e9a19896c73295b2fe5c520a1cb07cfe20f687deef9f294a0a05071e85c78a70a448ea5f0605" -def test_interpret_as_number_if_safely(): - assert _interpret_as_number_if_safely("") == 0 - assert _interpret_as_number_if_safely("0x5") == 5 - assert _interpret_as_number_if_safely("FF") == 255 - - def test_prepare_args_for_factories(): + sc = SmartContract(TransactionsFactoryConfig("mock")) args = [ "0x5", "123", "false", "true", "str:test-string", "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" ] - arguments = prepare_args_for_factory(args) + arguments = sc._prepare_args_for_factory(args) assert arguments[0] == b"\x05" assert arguments[1] == 123 - assert arguments[2] == False - assert arguments[3] == True + assert arguments[2] is False + assert arguments[3] is True assert arguments[4] == "test-string" assert arguments[5].to_bech32() == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" diff --git a/multiversx_sdk_cli/tests/test_native_auth_client.py b/multiversx_sdk_cli/tests/test_native_auth_client.py new file mode 100644 index 00000000..92678941 --- /dev/null +++ b/multiversx_sdk_cli/tests/test_native_auth_client.py @@ -0,0 +1,128 @@ +from typing import Any, List + +import pytest + +from multiversx_sdk_cli.native_auth_client import (NativeAuthClient, + NativeAuthClientConfig) + + +def mock(mocker: Any, code: int, response: Any): + mock_response = mocker.Mock() + mock_response.status_code = code + mock_response.json.return_value = response + mocker.patch("requests.get", return_value=mock_response) + + +def mock_side_effect(mocker: Any, responses: List[Any]): + def side_effect(*args: Any, **kwargs: Any): + response = responses.pop(0) + mock_response = mocker.Mock() + mock_response.status_code = response["code"] + mock_response.json.return_value = response["response"] + return mock_response + + mocker.patch("requests.get", side_effect=side_effect) + + +class TestNativeAuth: + ADDRESS = 'erd1qnk2vmuqywfqtdnkmauvpm8ls0xh00k8xeupuaf6cm6cd4rx89qqz0ppgl' + SIGNATURE = '906e79d54e69e688680abee54ec0c49ce2561eb5abfd01865b31cb3ed738272c7cfc4fc8cc1c3590dd5757e622639b01a510945d7f7c9d1ceda20a50a817080d' + BLOCK_HASH = 'ab459013b27fdc6fe98eed567bd0c1754e0628a4cc16883bf0170a29da37ad46' + TTL = 86400 + ORIGIN = 'https://api.multiversx.com' + TOKEN = f"aHR0cHM6Ly9hcGkubXVsdGl2ZXJzeC5jb20.{BLOCK_HASH}.{TTL}.e30" + ACCESS_TOKEN = 'ZXJkMXFuazJ2bXVxeXdmcXRkbmttYXV2cG04bHMweGgwMGs4eGV1cHVhZjZjbTZjZDRyeDg5cXF6MHBwZ2w.YUhSMGNITTZMeTloY0drdWJYVnNkR2wyWlhKemVDNWpiMjAuYWI0NTkwMTNiMjdmZGM2ZmU5OGVlZDU2N2JkMGMxNzU0ZTA2MjhhNGNjMTY4ODNiZjAxNzBhMjlkYTM3YWQ0Ni44NjQwMC5lMzA.906e79d54e69e688680abee54ec0c49ce2561eb5abfd01865b31cb3ed738272c7cfc4fc8cc1c3590dd5757e622639b01a510945d7f7c9d1ceda20a50a817080d' + INVALID_HASH_ERROR = 'Validation failed for block hash \'hash\'. Length should be 64.' + + def test_latest_block_should_return_signable_token(self, mocker: Any): + mock(mocker, 200, [{"hash": self.BLOCK_HASH}]) + config = NativeAuthClientConfig(origin=self.ORIGIN, expiry_seconds=self.TTL) + client = NativeAuthClient(config) + token = client.initialize() + assert token == self.TOKEN + + def test_throws_internal_server_error(self, mocker: Any): + mock(mocker, 500, {}) + client = NativeAuthClient() + with pytest.raises(Exception): + client.initialize() + + # if `/blocks/latest` raises error should fallback to `/blocks?size=1` + def test_fallback_mechanism(self, mocker: Any): + mock(mocker, 400, [{"statusCode": 400, + "message": self.INVALID_HASH_ERROR, + "error": "Bad request"}]) + mock(mocker, 200, {"hash": self.BLOCK_HASH}) + + config = NativeAuthClientConfig(origin=self.ORIGIN, expiry_seconds=self.TTL) + client = NativeAuthClient(config) + + token = client.initialize() + assert token == self.TOKEN + + def test_generate_access_token(self): + client = NativeAuthClient() + access_token = client.get_token(self.ADDRESS, self.TOKEN, self.SIGNATURE) + assert access_token == self.ACCESS_TOKEN + + +class TestNativeAuthWithGateway: + ADDRESS = 'erd1qnk2vmuqywfqtdnkmauvpm8ls0xh00k8xeupuaf6cm6cd4rx89qqz0ppgl' + SIGNATURE = '906e79d54e69e688680abee54ec0c49ce2561eb5abfd01865b31cb3ed738272c7cfc4fc8cc1c3590dd5757e622639b01a510945d7f7c9d1ceda20a50a817080d' + BLOCK_HASH = 'ab459013b27fdc6fe98eed567bd0c1754e0628a4cc16883bf0170a29da37ad46' + TTL = 86400 + ORIGIN = 'https://api.multiversx.com' + TOKEN = f"aHR0cHM6Ly9hcGkubXVsdGl2ZXJzeC5jb20.{BLOCK_HASH}.{TTL}.e30" + ACCESS_TOKEN = 'ZXJkMXFuazJ2bXVxeXdmcXRkbmttYXV2cG04bHMweGgwMGs4eGV1cHVhZjZjbTZjZDRyeDg5cXF6MHBwZ2w.YUhSMGNITTZMeTloY0drdWJYVnNkR2wyWlhKemVDNWpiMjAuYWI0NTkwMTNiMjdmZGM2ZmU5OGVlZDU2N2JkMGMxNzU0ZTA2MjhhNGNjMTY4ODNiZjAxNzBhMjlkYTM3YWQ0Ni44NjQwMC5lMzA.906e79d54e69e688680abee54ec0c49ce2561eb5abfd01865b31cb3ed738272c7cfc4fc8cc1c3590dd5757e622639b01a510945d7f7c9d1ceda20a50a817080d' + LATEST_ROUND = 115656 + METASHARD = 4294967295 + GATEWAY = 'https://gateway.multiversx.com' + + def test_latest_block_should_return_signable_token(self, mocker: Any): + responses = [ + {"code": 200, "response": {"data": {"status": {"erd_current_round": self.LATEST_ROUND}}}}, + {"code": 200, "response": {"data": {"blocks": [{"shard": self.METASHARD, "hash": self.BLOCK_HASH}]}}} + ] + mock_side_effect(mocker, responses) + + config = NativeAuthClientConfig(origin=self.ORIGIN, gateway_url=self.GATEWAY, block_hash_shard=self.METASHARD, expiry_seconds=self.TTL) + client = NativeAuthClient(config) + token = client.initialize() + assert token == self.TOKEN + + def test_should_raise_internal_server_error(self, mocker: Any): + responses = [ + {"code": 500, "response": {"data": {"status": {"erd_current_round": self.LATEST_ROUND}}}} + ] + mock_side_effect(mocker, responses) + + config = NativeAuthClientConfig(gateway_url=self.GATEWAY, block_hash_shard=self.METASHARD) + client = NativeAuthClient(config) + + with pytest.raises(Exception): + client.initialize() + + def test_raises_internal_server_error_on_second_request(self, mocker: Any): + responses = [ + {"code": 200, "response": {"data": {"status": {"erd_current_round": self.LATEST_ROUND}}}}, + {"code": 500, "response": {""}} + ] + mock_side_effect(mocker, responses) + + config = NativeAuthClientConfig(gateway_url=self.GATEWAY, block_hash_shard=self.METASHARD) + client = NativeAuthClient(config) + + with pytest.raises(Exception): + client.initialize() + + def test_generate_access_token(self): + config = NativeAuthClientConfig(gateway_url=self.GATEWAY, block_hash_shard=self.METASHARD) + client = NativeAuthClient(config) + + access_token = client.get_token( + address=self.ADDRESS, + token=self.TOKEN, + signature=self.SIGNATURE + ) + + assert access_token == self.ACCESS_TOKEN diff --git a/multiversx_sdk_cli/tests/test_proxy.py b/multiversx_sdk_cli/tests/test_proxy.py index 969aecd2..8cff09a5 100644 --- a/multiversx_sdk_cli/tests/test_proxy.py +++ b/multiversx_sdk_cli/tests/test_proxy.py @@ -1,6 +1,4 @@ -from multiversx_sdk_core import Address -from multiversx_sdk_network_providers.proxy_network_provider import \ - ProxyNetworkProvider +from multiversx_sdk import Address, ProxyNetworkProvider from multiversx_sdk_cli.accounts import Account from multiversx_sdk_cli.cli import main diff --git a/multiversx_sdk_cli/tests/test_shared.py b/multiversx_sdk_cli/tests/test_shared.py index 5df6c58f..caa43b3c 100644 --- a/multiversx_sdk_cli/tests/test_shared.py +++ b/multiversx_sdk_cli/tests/test_shared.py @@ -19,7 +19,7 @@ def test_prepare_chain_id_in_args(): prepare_chain_id_in_args(args) args.chain = "I" - args.proxy = "https://testnet-gateway.multiversx.com" + args.proxy = "https://testnet-api.multiversx.com" prepare_chain_id_in_args(args) assert args.chain == "T" diff --git a/multiversx_sdk_cli/tests/test_validators_core.py b/multiversx_sdk_cli/tests/test_validators_core.py index f3a7bcce..0066b345 100644 --- a/multiversx_sdk_cli/tests/test_validators_core.py +++ b/multiversx_sdk_cli/tests/test_validators_core.py @@ -1,6 +1,6 @@ from pathlib import Path -from multiversx_sdk_core import Address +from multiversx_sdk import Address from multiversx_sdk_cli.validators.core import \ prepare_transaction_data_for_stake diff --git a/multiversx_sdk_cli/tests/testdata/adder.abi.json b/multiversx_sdk_cli/tests/testdata/adder.abi.json new file mode 100644 index 00000000..56bbedc0 --- /dev/null +++ b/multiversx_sdk_cli/tests/testdata/adder.abi.json @@ -0,0 +1,72 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.79.0", + "commitHash": "129f3b9964af4d4a709d1383930ade12dfe7c081", + "commitDate": "2024-06-10", + "channel": "Stable", + "short": "rustc 1.79.0 (129f3b996 2024-06-10)" + }, + "contractCrate": { + "name": "adder", + "version": "0.0.0", + "gitVersion": "v0.45.2.1-reproducible-337-g5478c6e" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.51.1" + } + }, + "docs": [ + "One of the simplest smart contracts possible,", + "it holds a single variable in storage, which anyone can increment." + ], + "name": "Adder", + "constructor": { + "inputs": [ + { + "name": "initial_value", + "type": "BigUint" + } + ], + "outputs": [] + }, + "upgradeConstructor": { + "inputs": [ + { + "name": "initial_value", + "type": "BigUint" + } + ], + "outputs": [] + }, + "endpoints": [ + { + "name": "getSum", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "BigUint" + } + ] + }, + { + "docs": [ + "Add desired amount to the storage variable." + ], + "name": "add", + "mutability": "mutable", + "inputs": [ + { + "name": "value", + "type": "BigUint" + } + ], + "outputs": [] + } + ], + "esdtAttributes": [], + "hasCallback": false, + "types": {} +} diff --git a/multiversx_sdk_cli/tests/testdata/call_multisig_propose_batch_args.json b/multiversx_sdk_cli/tests/testdata/call_multisig_propose_batch_args.json new file mode 100644 index 00000000..02a1a89a --- /dev/null +++ b/multiversx_sdk_cli/tests/testdata/call_multisig_propose_batch_args.json @@ -0,0 +1,20 @@ +[ + [ + { + "__discriminant__": 5, + "0": { + "to": { + "bech32": "erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3" + }, + "egld_amount": 1000000000000000000, + "endpoint_name": "add", + "arguments": [ + { + "hex": "07" + } + ], + "opt_gas_limit": 15000000 + } + } + ] +] diff --git a/multiversx_sdk_cli/tests/testdata/deploy_multisig_args.json b/multiversx_sdk_cli/tests/testdata/deploy_multisig_args.json new file mode 100644 index 00000000..c70f879f --- /dev/null +++ b/multiversx_sdk_cli/tests/testdata/deploy_multisig_args.json @@ -0,0 +1,12 @@ +[ + 2, + [ + { + "bech32": "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th" + }, + { + "bech32": "erd1cqqxak4wun7508e0yj9ng843r6hv4mzd0hhpjpsejkpn9wa9yq8sj7u2u5" + } + + ] +] diff --git a/multiversx_sdk_cli/tests/testdata/multisig.abi.json b/multiversx_sdk_cli/tests/testdata/multisig.abi.json new file mode 100644 index 00000000..2493cc01 --- /dev/null +++ b/multiversx_sdk_cli/tests/testdata/multisig.abi.json @@ -0,0 +1,1302 @@ +{ + "buildInfo": { + "rustc": { + "version": "1.80.0-nightly", + "commitHash": "791adf759cc065316f054961875052d5bc03e16c", + "commitDate": "2024-05-21", + "channel": "Nightly", + "short": "rustc 1.80.0-nightly (791adf759 2024-05-21)" + }, + "contractCrate": { + "name": "multisig", + "version": "1.0.0", + "gitVersion": "v0.45.2.1-reproducible-309-ge0b3bbb" + }, + "framework": { + "name": "multiversx-sc", + "version": "0.50.4" + } + }, + "docs": [ + "Multi-signature smart contract implementation.", + "Acts like a wallet that needs multiple signers for any action performed.", + "See the readme file for more detailed documentation." + ], + "name": "Multisig", + "constructor": { + "inputs": [ + { + "name": "quorum", + "type": "u32" + }, + { + "name": "board", + "type": "variadic
", + "multi_arg": true + } + ], + "outputs": [] + }, + "upgradeConstructor": { + "inputs": [], + "outputs": [] + }, + "endpoints": [ + { + "docs": [ + "Allows the contract to receive funds even if it is marked as unpayable in the protocol." + ], + "name": "deposit", + "mutability": "mutable", + "payableInTokens": [ + "*" + ], + "inputs": [], + "outputs": [] + }, + { + "docs": [ + "Clears storage pertaining to an action that is no longer supposed to be executed.", + "Any signatures that the action received must first be removed, via `unsign`.", + "Otherwise this endpoint would be prone to abuse." + ], + "name": "discardAction", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Discard all the actions with the given IDs" + ], + "name": "discardBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "action_ids", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "docs": [ + "Minimum number of signatures needed to perform any action." + ], + "name": "getQuorum", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Denormalized board member count.", + "It is kept in sync with the user list by the contract." + ], + "name": "getNumBoardMembers", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "getNumGroups", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Denormalized proposer count.", + "It is kept in sync with the user list by the contract." + ], + "name": "getNumProposers", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "getActionGroup", + "mutability": "readonly", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "variadic", + "multi_result": true + } + ] + }, + { + "name": "getLastGroupActionId", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "The index of the last proposed action.", + "0 means that no action was ever proposed yet." + ], + "name": "getActionLastIndex", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Initiates board member addition process.", + "Can also be used to promote a proposer to board member." + ], + "name": "proposeAddBoardMember", + "mutability": "mutable", + "inputs": [ + { + "name": "board_member_address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Initiates proposer addition process..", + "Can also be used to demote a board member to proposer." + ], + "name": "proposeAddProposer", + "mutability": "mutable", + "inputs": [ + { + "name": "proposer_address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Removes user regardless of whether it is a board member or proposer." + ], + "name": "proposeRemoveUser", + "mutability": "mutable", + "inputs": [ + { + "name": "user_address", + "type": "Address" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeChangeQuorum", + "mutability": "mutable", + "inputs": [ + { + "name": "new_quorum", + "type": "u32" + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Propose a transaction in which the contract will perform a transfer-execute call.", + "Can send EGLD without calling anything.", + "Can call smart contract endpoints directly.", + "Doesn't really work with builtin functions." + ], + "name": "proposeTransferExecute", + "mutability": "mutable", + "inputs": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "egld_amount", + "type": "BigUint" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "function_call", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeTransferExecuteEsdt", + "mutability": "mutable", + "inputs": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "tokens", + "type": "List" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "function_call", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Propose a transaction in which the contract will perform an async call call.", + "Can call smart contract endpoints directly.", + "Can use ESDTTransfer/ESDTNFTTransfer/MultiESDTTransfer to send tokens, while also optionally calling endpoints.", + "Works well with builtin functions.", + "Cannot simply send EGLD directly without calling anything." + ], + "name": "proposeAsyncCall", + "mutability": "mutable", + "inputs": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "egld_amount", + "type": "BigUint" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "function_call", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeSCDeployFromSource", + "mutability": "mutable", + "inputs": [ + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "source", + "type": "Address" + }, + { + "name": "code_metadata", + "type": "CodeMetadata" + }, + { + "name": "arguments", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeSCUpgradeFromSource", + "mutability": "mutable", + "inputs": [ + { + "name": "sc_address", + "type": "Address" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "source", + "type": "Address" + }, + { + "name": "code_metadata", + "type": "CodeMetadata" + }, + { + "name": "arguments", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "name": "proposeBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "actions", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "u32" + } + ] + }, + { + "docs": [ + "Used by board members to sign actions." + ], + "name": "sign", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Sign all the actions in the given batch" + ], + "name": "signBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "name": "signAndPerform", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "optional
", + "multi_result": true + } + ] + }, + { + "name": "signBatchAndPerform", + "mutability": "mutable", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Board members can withdraw their signatures if they no longer desire for the action to be executed.", + "Actions that are left with no valid signatures can be then deleted to free up storage." + ], + "name": "unsign", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Unsign all actions with the given IDs" + ], + "name": "unsignBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "docs": [ + "Returns `true` (`1`) if the user has signed the action.", + "Does not check whether or not the user is still a board member and the signature valid." + ], + "name": "signed", + "mutability": "readonly", + "inputs": [ + { + "name": "user", + "type": "Address" + }, + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "name": "unsignForOutdatedBoardMembers", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + }, + { + "name": "outdated_board_members", + "type": "variadic", + "multi_arg": true + } + ], + "outputs": [] + }, + { + "docs": [ + "Returns `true` (`1`) if `getActionValidSignerCount >= getQuorum`." + ], + "name": "quorumReached", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "bool" + } + ] + }, + { + "docs": [ + "Proposers and board members use this to launch signed actions." + ], + "name": "performAction", + "mutability": "mutable", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "optional
", + "multi_result": true + } + ] + }, + { + "docs": [ + "Perform all the actions in the given batch" + ], + "name": "performBatch", + "mutability": "mutable", + "inputs": [ + { + "name": "group_id", + "type": "u32" + } + ], + "outputs": [] + }, + { + "name": "dnsRegister", + "onlyOwner": true, + "mutability": "mutable", + "payableInTokens": [ + "EGLD" + ], + "inputs": [ + { + "name": "dns_address", + "type": "Address" + }, + { + "name": "name", + "type": "bytes" + } + ], + "outputs": [] + }, + { + "docs": [ + "Iterates through all actions and retrieves those that are still pending.", + "Serialized full action data:", + "- the action id", + "- the serialized action data", + "- (number of signers followed by) list of signer addresses." + ], + "name": "getPendingActionFullInfo", + "mutability": "readonly", + "inputs": [ + { + "name": "opt_range", + "type": "optional>", + "multi_arg": true + } + ], + "outputs": [ + { + "type": "variadic", + "multi_result": true + } + ], + "labels": [ + "multisig-external-view" + ], + "allow_multiple_var_args": true + }, + { + "docs": [ + "Indicates user rights.", + "`0` = no rights,", + "`1` = can propose, but not sign,", + "`2` = can propose and sign." + ], + "name": "userRole", + "mutability": "readonly", + "inputs": [ + { + "name": "user", + "type": "Address" + } + ], + "outputs": [ + { + "type": "UserRole" + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Lists all users that can sign actions." + ], + "name": "getAllBoardMembers", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Lists all proposers that are not board members." + ], + "name": "getAllProposers", + "mutability": "readonly", + "inputs": [], + "outputs": [ + { + "type": "variadic
", + "multi_result": true + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Serialized action data of an action with index." + ], + "name": "getActionData", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "Action" + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Gets addresses of all users who signed an action.", + "Does not check if those users are still board members or not,", + "so the result may contain invalid signers." + ], + "name": "getActionSigners", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "List
" + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "Gets addresses of all users who signed an action and are still board members.", + "All these signatures are currently valid." + ], + "name": "getActionSignerCount", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "u32" + } + ], + "labels": [ + "multisig-external-view" + ] + }, + { + "docs": [ + "It is possible for board members to lose their role.", + "They are not automatically removed from all actions when doing so,", + "therefore the contract needs to re-check every time when actions are performed.", + "This function is used to validate the signers before performing an action.", + "It also makes it easy to check before performing an action." + ], + "name": "getActionValidSignerCount", + "mutability": "readonly", + "inputs": [ + { + "name": "action_id", + "type": "u32" + } + ], + "outputs": [ + { + "type": "u32" + } + ], + "labels": [ + "multisig-external-view" + ] + } + ], + "events": [ + { + "identifier": "asyncCallSuccess", + "inputs": [ + { + "name": "results", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "asyncCallError", + "inputs": [ + { + "name": "err_code", + "type": "u32", + "indexed": true + }, + { + "name": "err_message", + "type": "bytes", + "indexed": true + } + ] + }, + { + "identifier": "startPerformAction", + "inputs": [ + { + "name": "data", + "type": "ActionFullInfo" + } + ] + }, + { + "identifier": "performChangeUser", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "changed_user", + "type": "Address", + "indexed": true + }, + { + "name": "old_role", + "type": "UserRole", + "indexed": true + }, + { + "name": "new_role", + "type": "UserRole", + "indexed": true + } + ] + }, + { + "identifier": "performChangeQuorum", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "new_quorum", + "type": "u32", + "indexed": true + } + ] + }, + { + "identifier": "performAsyncCall", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "to", + "type": "Address", + "indexed": true + }, + { + "name": "egld_value", + "type": "BigUint", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "endpoint", + "type": "bytes", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "performTransferExecuteEgld", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "to", + "type": "Address", + "indexed": true + }, + { + "name": "egld_value", + "type": "BigUint", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "endpoint", + "type": "bytes", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "performTransferExecuteEsdt", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "to", + "type": "Address", + "indexed": true + }, + { + "name": "tokens", + "type": "List", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "endpoint", + "type": "bytes", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "performDeployFromSource", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "egld_value", + "type": "BigUint", + "indexed": true + }, + { + "name": "source_address", + "type": "Address", + "indexed": true + }, + { + "name": "code_metadata", + "type": "CodeMetadata", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + }, + { + "identifier": "performUpgradeFromSource", + "inputs": [ + { + "name": "action_id", + "type": "u32", + "indexed": true + }, + { + "name": "target_address", + "type": "Address", + "indexed": true + }, + { + "name": "egld_value", + "type": "BigUint", + "indexed": true + }, + { + "name": "source_address", + "type": "Address", + "indexed": true + }, + { + "name": "code_metadata", + "type": "CodeMetadata", + "indexed": true + }, + { + "name": "gas", + "type": "u64", + "indexed": true + }, + { + "name": "arguments", + "type": "variadic", + "indexed": true + } + ] + } + ], + "esdtAttributes": [], + "hasCallback": true, + "types": { + "Action": { + "type": "enum", + "variants": [ + { + "name": "Nothing", + "discriminant": 0 + }, + { + "name": "AddBoardMember", + "discriminant": 1, + "fields": [ + { + "name": "0", + "type": "Address" + } + ] + }, + { + "name": "AddProposer", + "discriminant": 2, + "fields": [ + { + "name": "0", + "type": "Address" + } + ] + }, + { + "name": "RemoveUser", + "discriminant": 3, + "fields": [ + { + "name": "0", + "type": "Address" + } + ] + }, + { + "name": "ChangeQuorum", + "discriminant": 4, + "fields": [ + { + "name": "0", + "type": "u32" + } + ] + }, + { + "name": "SendTransferExecuteEgld", + "discriminant": 5, + "fields": [ + { + "name": "0", + "type": "CallActionData" + } + ] + }, + { + "name": "SendTransferExecuteEsdt", + "discriminant": 6, + "fields": [ + { + "name": "0", + "type": "EsdtTransferExecuteData" + } + ] + }, + { + "name": "SendAsyncCall", + "discriminant": 7, + "fields": [ + { + "name": "0", + "type": "CallActionData" + } + ] + }, + { + "name": "SCDeployFromSource", + "discriminant": 8, + "fields": [ + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "source", + "type": "Address" + }, + { + "name": "code_metadata", + "type": "CodeMetadata" + }, + { + "name": "arguments", + "type": "List" + } + ] + }, + { + "name": "SCUpgradeFromSource", + "discriminant": 9, + "fields": [ + { + "name": "sc_address", + "type": "Address" + }, + { + "name": "amount", + "type": "BigUint" + }, + { + "name": "source", + "type": "Address" + }, + { + "name": "code_metadata", + "type": "CodeMetadata" + }, + { + "name": "arguments", + "type": "List" + } + ] + } + ] + }, + "ActionFullInfo": { + "type": "struct", + "docs": [ + "Not used internally, just to retrieve results via endpoint." + ], + "fields": [ + { + "name": "action_id", + "type": "u32" + }, + { + "name": "group_id", + "type": "u32" + }, + { + "name": "action_data", + "type": "Action" + }, + { + "name": "signers", + "type": "List
" + } + ] + }, + "ActionStatus": { + "type": "enum", + "variants": [ + { + "name": "Available", + "discriminant": 0 + }, + { + "name": "Aborted", + "discriminant": 1 + } + ] + }, + "CallActionData": { + "type": "struct", + "fields": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "egld_amount", + "type": "BigUint" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "endpoint_name", + "type": "bytes" + }, + { + "name": "arguments", + "type": "List" + } + ] + }, + "EsdtTokenPayment": { + "type": "struct", + "fields": [ + { + "name": "token_identifier", + "type": "TokenIdentifier" + }, + { + "name": "token_nonce", + "type": "u64" + }, + { + "name": "amount", + "type": "BigUint" + } + ] + }, + "EsdtTransferExecuteData": { + "type": "struct", + "fields": [ + { + "name": "to", + "type": "Address" + }, + { + "name": "tokens", + "type": "List" + }, + { + "name": "opt_gas_limit", + "type": "Option" + }, + { + "name": "endpoint_name", + "type": "bytes" + }, + { + "name": "arguments", + "type": "List" + } + ] + }, + "UserRole": { + "type": "enum", + "variants": [ + { + "name": "None", + "discriminant": 0 + }, + { + "name": "Proposer", + "discriminant": 1 + }, + { + "name": "BoardMember", + "discriminant": 2 + } + ] + } + } +} diff --git a/multiversx_sdk_cli/tests/testdata/multisig.wasm b/multiversx_sdk_cli/tests/testdata/multisig.wasm new file mode 100644 index 00000000..e7cc7b72 Binary files /dev/null and b/multiversx_sdk_cli/tests/testdata/multisig.wasm differ diff --git a/multiversx_sdk_cli/tests/testdata/upgrade_multisig_args.json b/multiversx_sdk_cli/tests/testdata/upgrade_multisig_args.json new file mode 100644 index 00000000..33731ca0 --- /dev/null +++ b/multiversx_sdk_cli/tests/testdata/upgrade_multisig_args.json @@ -0,0 +1,15 @@ +[ + { + "bech32": "erd1qqqqqqqqqqqqqpgqpg2tnjelx3s3dhkksqhjavprtgmt9xt4d8ss9al8m4" + }, + 0, + { + "bech32": "erd1qqqqqqqqqqqqqpgqak8zt22wl2ph4tswtyc39namqx6ysa2sd8ss4xmlj3" + }, + { + "hex": "0500" + }, + [ + 0 + ] +] diff --git a/multiversx_sdk_cli/transactions.py b/multiversx_sdk_cli/transactions.py index 6f32c511..1e441a17 100644 --- a/multiversx_sdk_cli/transactions.py +++ b/multiversx_sdk_cli/transactions.py @@ -2,9 +2,13 @@ import json import logging import time -from typing import Any, Dict, Optional, Protocol, TextIO +from pathlib import Path +from typing import Any, Dict, List, Optional, Protocol, TextIO -from multiversx_sdk_core import Address, Transaction, TransactionPayload +from multiversx_sdk import (Address, Token, TokenComputer, TokenTransfer, + Transaction, TransactionsConverter, + TransactionsFactoryConfig, + TransferTransactionsFactory) from multiversx_sdk_cli import errors from multiversx_sdk_cli.accounts import Account, LedgerAccount @@ -34,27 +38,60 @@ def get_transaction(self, tx_hash: str, with_process_status: Optional[bool] = Fa ... -class JSONTransaction: - def __init__(self) -> None: - self.hash = "" - self.nonce = 0 - self.value = "0" - self.receiver = "" - self.sender = "" - self.senderUsername = "" - self.receiverUsername = "" - self.gasPrice = 0 - self.gasLimit = 0 - self.data: str = "" - self.chainID = "" - self.version = 0 - self.options = 0 - self.signature = "" - self.guardian = "" - self.guardianSignature = "" +def do_prepare_transaction(args: Any) -> Transaction: + account = load_sender_account_from_args(args) + + native_amount = int(args.value) + transfers = getattr(args, "token_transfers", []) + transfers = prepare_token_transfers(transfers) if transfers else None + + config = TransactionsFactoryConfig(args.chain) + factory = TransferTransactionsFactory(config) + receiver = Address.new_from_bech32(args.receiver) + + if native_amount or transfers: + tx = factory.create_transaction_for_transfer( + sender=account.address, + receiver=receiver, + native_amount=native_amount, + token_transfers=transfers, + data=str(args.data).encode() + ) + else: + # this is for transactions with no token transfers(egld/esdt); useful for setting the data field + tx = Transaction( + sender=account.address.to_bech32(), + receiver=receiver.to_bech32(), + data=str(args.data).encode(), + gas_limit=int(args.gas_limit), + chain_id=args.chain + ) + + tx.gas_limit = int(args.gas_limit) + tx.sender_username = getattr(args, "sender_username", None) or "" + tx.receiver_username = getattr(args, "receiver_username", None) or "" + tx.gas_price = int(args.gas_price) + tx.nonce = int(args.nonce) + tx.value = int(args.value) + tx.version = int(args.version) + tx.options = int(args.options) + if args.guardian: + tx.guardian = args.guardian -def do_prepare_transaction(args: Any) -> Transaction: + if args.relayer: + tx.relayer = Address.new_from_bech32(args.relayer).to_bech32() + + if args.inner_transactions: + tx.inner_transactions = load_inner_transactions_from_file(Path(args.inner_transactions).expanduser()) + + tx.signature = bytes.fromhex(account.sign_transaction(tx)) + tx = sign_tx_by_guardian(args, tx) + + return tx + + +def load_sender_account_from_args(args: Any) -> Account: account = Account() if args.ledger: account = LedgerAccount(account_index=args.ledger_account_index, address_index=args.ledger_address_index) @@ -64,28 +101,23 @@ def do_prepare_transaction(args: Any) -> Transaction: password = load_password(args) account = Account(key_file=args.keyfile, password=password) - tx = Transaction( - chain_id=args.chain, - sender=account.address.to_bech32(), - receiver=args.receiver, - gas_limit=int(args.gas_limit), - sender_username=getattr(args, "sender_username", ""), - receiver_username=getattr(args, "receiver_username", ""), - gas_price=int(args.gas_price), - data=str(args.data).encode(), - nonce=int(args.nonce), - value=int(args.value), - version=int(args.version), - options=int(args.options) - ) + return account - if args.guardian: - tx.guardian = args.guardian - tx.signature = bytes.fromhex(account.sign_transaction(tx)) - tx = sign_tx_by_guardian(args, tx) +def prepare_token_transfers(transfers: List[Any]) -> List[TokenTransfer]: + token_computer = TokenComputer() + token_transfers: List[TokenTransfer] = [] - return tx + for i in range(0, len(transfers) - 1, 2): + identifier = transfers[i] + amount = int(transfers[i + 1]) + nonce = token_computer.extract_nonce_from_extended_identifier(identifier) + + token = Token(identifier, nonce) + transfer = TokenTransfer(token, amount) + token_transfers.append(transfer) + + return token_transfers def sign_tx_by_guardian(args: Any, tx: Transaction) -> Transaction: @@ -172,7 +204,7 @@ def tx_to_dictionary_as_inner_for_relayed_V1(tx: Transaction) -> Dict[str, Any]: dictionary["sndUserName"] = base64.b64encode(tx.sender_username.encode()).decode() if tx.receiver_username: - dictionary[f"rcvUserName"] = base64.b64encode(tx.receiver_username.encode()).decode() + dictionary["rcvUserName"] = base64.b64encode(tx.receiver_username.encode()).decode() return dictionary @@ -191,37 +223,15 @@ def compute_relayed_v1_data(tx: Transaction) -> str: def load_transaction_from_file(f: TextIO) -> Transaction: data_json: bytes = f.read().encode() - fields = json.loads(data_json).get("tx") or json.loads(data_json).get("emittedTransaction") - - instance = JSONTransaction() - instance.__dict__.update(fields) - - loaded_tx = Transaction( - chain_id=instance.chainID, - sender=instance.sender, - receiver=instance.receiver, - sender_username=decode_field_value(instance.senderUsername), - receiver_username=decode_field_value(instance.receiverUsername), - gas_limit=instance.gasLimit, - gas_price=instance.gasPrice, - value=int(instance.value), - data=TransactionPayload.from_encoded_str(instance.data).data, - version=instance.version, - options=instance.options, - nonce=instance.nonce - ) - - if instance.guardian: - loaded_tx.guardian = instance.guardian - - if instance.signature: - loaded_tx.signature = bytes.fromhex(instance.signature) + transaction_dictionary = json.loads(data_json).get("tx") or json.loads(data_json).get("emittedTransaction") - if instance.guardianSignature: - loaded_tx.guardian_signature = bytes.fromhex(instance.guardianSignature) + tx_converter = TransactionsConverter() + return tx_converter.dictionary_to_transaction(transaction_dictionary) - return loaded_tx +def load_inner_transactions_from_file(path: Path) -> List[Transaction]: + data_json = path.read_text() + transactions: List[Dict[str, Any]] = json.loads(data_json).get("innerTransactions") -def decode_field_value(value: str) -> str: - return base64.b64decode(value).decode() + tx_converter = TransactionsConverter() + return [tx_converter.dictionary_to_transaction(transaction) for transaction in transactions] diff --git a/multiversx_sdk_cli/utils.py b/multiversx_sdk_cli/utils.py index f2af91ef..a61e41fa 100644 --- a/multiversx_sdk_cli/utils.py +++ b/multiversx_sdk_cli/utils.py @@ -11,10 +11,8 @@ from typing import (Any, Dict, List, Optional, Protocol, Union, runtime_checkable) -import requests_cache import toml -import multiversx_sdk_cli.config from multiversx_sdk_cli import errors logger = logging.getLogger("utils") @@ -39,9 +37,11 @@ def to_json(self): class BasicEncoder(json.JSONEncoder): - def default(self, o: Any): + def default(self, o: Any) -> Any: if isinstance(o, ISerializable): return o.to_dictionary() + if isinstance(o, bytes): + return o.hex() return super().default(o) @@ -149,17 +149,6 @@ def mark_executable(file: str) -> None: os.chmod(file, st.st_mode | stat.S_IEXEC) -def find_in_dictionary(dictionary, compound_path): - keys = compound_path.split(".") - node = dictionary - for key in keys: - node = node.get(key) - if node is None: - break - - return node - - def list_files(folder: Path, suffix: Optional[str] = None) -> List[Path]: folder = folder.expanduser() files: List[Path] = [folder / file for file in os.listdir(folder)] @@ -210,7 +199,7 @@ def str_int_to_hex_str(number_str: str) -> str: return bytes_str -def parse_keys(bls_public_keys): +def parse_keys(bls_public_keys: str): keys = bls_public_keys.split(',') parsed_keys = '' for key in keys: diff --git a/multiversx_sdk_cli/validators/__init__.py b/multiversx_sdk_cli/validators/__init__.py index c66ffcba..74a11e54 100644 --- a/multiversx_sdk_cli/validators/__init__.py +++ b/multiversx_sdk_cli/validators/__init__.py @@ -1,16 +1,11 @@ -from multiversx_sdk_cli.validators.core import (prepare_args_for_change_reward_address, - prepare_args_for_claim, - prepare_args_for_stake, - prepare_args_for_unbond, - prepare_args_for_unjail, - prepare_args_for_unstake, - prepare_args_for_unstake_nodes, - prepare_args_for_unstake_tokens, - prepare_args_for_unbond_tokens, - prepare_args_for_unbond_nodes, - prepare_args_for_clean_registered_data, - prepare_args_for_restake_unstaked_nodes) - +from multiversx_sdk_cli.validators.core import ( + prepare_args_for_change_reward_address, prepare_args_for_claim, + prepare_args_for_clean_registered_data, + prepare_args_for_restake_unstaked_nodes, prepare_args_for_stake, + prepare_args_for_unbond, prepare_args_for_unbond_nodes, + prepare_args_for_unbond_tokens, prepare_args_for_unjail, + prepare_args_for_unstake, prepare_args_for_unstake_nodes, + prepare_args_for_unstake_tokens) from multiversx_sdk_cli.validators.validators_file import ValidatorsFile __all__ = ["prepare_args_for_stake", diff --git a/multiversx_sdk_cli/validators/core.py b/multiversx_sdk_cli/validators/core.py index 61d0c136..0e6e9f95 100644 --- a/multiversx_sdk_cli/validators/core.py +++ b/multiversx_sdk_cli/validators/core.py @@ -2,9 +2,7 @@ from pathlib import Path from typing import Any, List, Tuple, Union -from multiversx_sdk_core import Address -from multiversx_sdk_wallet.validator_pem import ValidatorPEM -from multiversx_sdk_wallet.validator_signer import ValidatorSigner +from multiversx_sdk import Address, ValidatorPEM, ValidatorSigner from multiversx_sdk_cli import utils from multiversx_sdk_cli.accounts import Account diff --git a/multiversx_sdk_cli/validators/validators_file.py b/multiversx_sdk_cli/validators/validators_file.py index 4a664aac..ab10535a 100644 --- a/multiversx_sdk_cli/validators/validators_file.py +++ b/multiversx_sdk_cli/validators/validators_file.py @@ -2,9 +2,7 @@ from pathlib import Path from typing import Dict, List -from multiversx_sdk_wallet import ValidatorSigner -from multiversx_sdk_wallet.validator_keys import ValidatorPublicKey -from multiversx_sdk_wallet.validator_pem import ValidatorPEM +from multiversx_sdk import ValidatorPEM, ValidatorPublicKey, ValidatorSigner from multiversx_sdk_cli import guards from multiversx_sdk_cli.errors import CannotReadValidatorsData diff --git a/multiversx_sdk_cli/version.py b/multiversx_sdk_cli/version.py index fa7c33f3..df09e4ce 100644 --- a/multiversx_sdk_cli/version.py +++ b/multiversx_sdk_cli/version.py @@ -9,7 +9,7 @@ def get_version() -> str: try: # Works for local development return _get_version_from_pyproject() - except Exception as error: + except Exception: try: # Works for the installed package return _get_version_from_metadata() diff --git a/mxpy-up.py b/mxpy-up.py index 8cfacc3e..24177d3a 100644 --- a/mxpy-up.py +++ b/mxpy-up.py @@ -21,9 +21,15 @@ def main(): parser.add_argument("--from-branch", help="use a branch of multiversx/mx-sdk-py-cli") parser.add_argument("--not-interactive", action="store_true", default=False) parser.add_argument("--verbose", action="store_true", default=False) + parser.add_argument("--ignore-deprecation", action="store_true", default=False, help="'mxpy-up.py' is obsolete, install using 'pipx': https://docs.multiversx.com/sdk-and-tools/sdk-py/installing-mxpy/#install-using-pipx") parser.set_defaults(modify_path=True) args = parser.parse_args() + logger.warning("'mxpy-up.py' is deprecated. Check out the documentation on how to install using `pipx`: https://docs.multiversx.com/sdk-and-tools/sdk-py/installing-mxpy/#install-using-pipx.") + + if not args.ignore_deprecation: + raise Exception("'mxpy-up.py' is deprecated, please install using `pipx`: https://docs.multiversx.com/sdk-and-tools/sdk-py/installing-mxpy/#install-using-pipx. If installing using 'mxpy-up` is necessary, provide the `--ignore-deprecation` flag.") + exact_version = args.exact_version from_branch = args.from_branch interactive = not args.not_interactive @@ -56,6 +62,8 @@ def main(): if interactive: guide_system_path_integration() + logger.warning("Installing `mxpy` using `mxpy-up.py` is deprecated. Check out the documentation on how to install using `pipx`: https://docs.multiversx.com/sdk-and-tools/sdk-py/installing-mxpy/#install-using-pipx") + def guard_non_root_user(): logger.debug("Checking user (should not be root).") @@ -260,7 +268,7 @@ def guide_system_path_integration(): return old_export_directive = f'export PATH="{Path("~/elrondsdk").expanduser()}:$PATH"\t# elrond-sdk' - new_export_directive = f'export PATH="${{HOME}}/multiversx-sdk:$PATH"\t# multiversx-sdk' + new_export_directive = 'export PATH="${{HOME}}/multiversx-sdk:$PATH"\t# multiversx-sdk' profile_files = get_profile_files() @@ -292,7 +300,7 @@ def guide_system_path_integration(): Your shell profile files: {profile_files_formatted} -The entry (entries) to remove: +The entry (entries) to remove: {old_export_directive} ############################################################################### Make sure you UNDERSTAND the above before proceeding further. @@ -306,12 +314,12 @@ def guide_system_path_integration(): ############################################################################### It seems that the path "~/multiversx-sdk" is already configured in shell profile. -To confirm this, CHECK the shell profile (now or after the installer script ends). +To confirm this, CHECK the shell profile (now or after the installer script ends). Your shell profile files: {profile_files_formatted} -The entry to check (it should be present): +The entry to check (it should be present): {new_export_directive}. ############################################################################### Make sure you UNDERSTAND the above before proceeding further. diff --git a/pyproject.toml b/pyproject.toml index 12abff1d..c90a4f59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "multiversx-sdk-cli" -version = "9.6.3" +version = "9.7.0" authors = [ { name="MultiversX" }, ] @@ -21,15 +21,14 @@ classifiers = [ dependencies = [ "toml>=0.10.2", - "requests", + "requests>=2.32.0,<3.0.0", "prettytable", "ledgercomm[hid]", "semver", "requests-cache", "rich==13.3.4", - "multiversx-sdk-network-providers>=0.13.0,<0.14.0", - "multiversx-sdk-wallet>=0.9.0,<0.10.0", - "multiversx-sdk-core>=0.8.0,<0.9.0" + "argcomplete==3.2.2", + "multiversx-sdk==0.13.0" ] [project.scripts] diff --git a/requirements-dev.txt b/requirements-dev.txt index 85d17fda..ce2c47ff 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,3 +1,4 @@ pytest flake8 autopep8 +pytest-mock diff --git a/requirements.txt b/requirements.txt index 26f1d5b4..2048553c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ toml>=0.10.2 types-toml -requests~=2.31.0 +requests>=2.32.0,<3.0.0 types-requests prettytable types-prettytable @@ -8,7 +8,6 @@ ledgercomm[hid] semver requests-cache rich==13.3.4 +argcomplete==3.2.2 -multiversx-sdk-core>=0.8.0,<0.9.0 -multiversx-sdk-network-providers>=0.13.0,<0.14.0 -multiversx-sdk-wallet>=0.9.0,<0.10.0 +multiversx-sdk==0.13.0