Repository: multiversx/mx-sdk-py-cli (language: python)

Issue:
Bump version (v7)



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build-windows.yml b/.github/workflows/build-windows.yml
index 475463bd..4dfa1a4e 100644
--- a/.github/workflows/build-windows.yml
+++ b/.github/workflows/build-windows.yml
@@ -55,6 +55,5 @@ jobs:
         export CHAIN_ID=T
         cd ./multiversx_sdk_cli/tests
         source ./test_cli_tx.sh && testAll || return 1
-        source ./test_cli_wallet.sh && testAll || return 1
         source ./test_cli_dns.sh && testOffline || return 1
         source ./test_cli_validators.sh && testAll || return 1
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index e9e1f3d9..f04afedb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -55,5 +55,4 @@ jobs:
         python3 -m multiversx_sdk_cli.cli config set dependencies.vmtools.tag v1.4.60
         cd ./multiversx_sdk_cli/tests
         source ./test_cli_contracts.sh && testAll || return 1
-        source ./test_cli_wallet.sh && testAll || return 1
         source ./test_cli_dns.sh && testOffline || return 1
diff --git a/.github/workflows/migrate.yml b/.github/workflows/migrate.yml
index 3cb01aab..fee7e6d6 100644
--- a/.github/workflows/migrate.yml
+++ b/.github/workflows/migrate.yml
@@ -35,7 +35,8 @@ jobs:
 
           erdpy config new test
           erdpy config set github_api_token ${{ secrets.GITHUB_TOKEN }}
-          erdpy deps install wasm-opt
+          erdpy config set dependencies.wasm-opt.tag 1.4.0
+          erdpy deps install wasm-opt --overwrite
 
           erdpy contract new --template=adder adder
           erdpy contract build adder
@@ -47,7 +48,6 @@ jobs:
 
           export PATH="~/multiversx-sdk:${PATH}"
 
-          mxpy deps install wasm-opt
           mxpy contract new --template=lottery-esdt lottery-esdt
           mxpy contract build lottery-esdt
           mxpy contract test --directory="scenarios" adder
diff --git a/CLI.md b/CLI.md
index 94320748..a7bbb5fe 100644
--- a/CLI.md
+++ b/CLI.md
@@ -20,7 +20,7 @@ https://docs.multiversx.com/sdk-and-tools/mxpy.
         
 
 COMMAND GROUPS:
-  {contract,tx,validator,account,ledger,wallet,network,deps,config,hyperblock,testnet,data,staking-provider,dns}
+  {contract,tx,validator,account,ledger,wallet,network,deps,config,hyperblock,localnet,data,staking-provider,dns}
 
 TOP-LEVEL OPTIONS:
   -h, --help            show this help message and exit
@@ -40,7 +40,7 @@ network                        Get Network parameters, such as number of shards,
 deps                           Manage dependencies or multiversx-sdk modules
 config                         Configure multiversx-sdk (default values etc.)
 hyperblock                     Get Hyperblock from the Network
-testnet                        Set up, start and control local testnets
+localnet                       Set up, start and control localnets
 data                           Data manipulation omnitool
 staking-provider               Staking provider omnitool
 dns                            Operations related to the Domain Name Service
@@ -90,7 +90,7 @@ Create a new Smart Contract project based on a template.
 positional arguments:
   name
 
-optional arguments:
+options:
   -h, --help             show this help message and exit
   --template TEMPLATE    the template to use
   --directory DIRECTORY  🗀 the parent directory of the project (default: current directory)
@@ -105,7 +105,7 @@ usage: mxpy contract templates [-h] ...
 
 List the available Smart Contract templates.
 
-optional arguments:
+options:
   -h, --help  show this help message and exit
 
 ```
@@ -121,7 +121,7 @@ Build a Smart Contract project using the appropriate buildchain.
 positional arguments:
   project                              🗀 the project directory (default: current directory)
 
-optional arguments:
+options:
   -h, --help                           show this help message and exit
   -r, --recursive                      locate projects recursively
   --debug                              set debug flag (default: False)
@@ -133,9 +133,6 @@ optional arguments:
                                        False)
   --wasm-name WASM_NAME                for rust projects, optionally specify the name of the wasm bytecode output file
   --wasm-suffix WASM_SUFFIX            for rust projects, optionally specify the suffix of the wasm bytecode output file
-  --eei-checks                         run EEI compatibility checks (default: False)
-  --skip-eei-checks                    deprecated flag
-  --ignore-eei-checks                  deprecated flag
 
 ```
 ### Contract.Clean
@@ -150,7 +147,7 @@ Clean a Smart Contract project.
 positional arguments:
   project          🗀 the project directory (default: current directory)
 
-optional arguments:
+options:
   -h, --help       show this help message and exit
   -r, --recursive  locate projects recursively
 
@@ -192,9 +189,8 @@ Output example:
     }
 }
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
-  --project PROJECT                               🗀 the project directory (default: current directory)
   --bytecode BYTECODE                             the file containing the WASM bytecode
   --metadata-not-upgradeable                      ‼ mark the contract as NOT upgradeable (default: upgradeable)
   --metadata-not-readable                         ‼ mark the contract as NOT readable (default: readable)
@@ -283,7 +279,7 @@ Output example:
 positional arguments:
   contract                                        🖄 the address of the Smart Contract
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --outfile OUTFILE                               where to save the output (default: stdout)
   --pem PEM                                       🔑 the PEM file, if keyfile not provided
@@ -370,10 +366,9 @@ Output example:
 positional arguments:
   contract                                        🖄 the address of the Smart Contract
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --outfile OUTFILE                               where to save the output (default: stdout)
-  --project PROJECT                               🗀 the project directory (default: current directory)
   --bytecode BYTECODE                             the file containing the WASM bytecode
   --metadata-not-upgradeable                      ‼ mark the contract as NOT upgradeable (default: upgradeable)
   --metadata-not-readable                         ‼ mark the contract as NOT readable (default: readable)
@@ -433,7 +428,7 @@ Query a Smart Contract (call a pure function)
 positional arguments:
   contract                               🖄 the address of the Smart Contract
 
-optional arguments:
+options:
   -h, --help                             show this help message and exit
   --proxy PROXY                          🔗 the URL of the proxy (default: https://testnet-gateway.multiversx.com)
   --function FUNCTION                    the function to call
@@ -454,7 +449,7 @@ Print a detailed report of the smart contracts.
 positional arguments:
   project                                         🗀 the project directory (default: current directory)
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --skip-build                                    skips the step of building of the wasm contracts
   --skip-twiggy                                   skips the steps of building the debug wasm files and running twiggy
@@ -474,9 +469,6 @@ optional arguments:
                                                   output file
   --wasm-suffix WASM_SUFFIX                       for rust projects, optionally specify the suffix of the wasm bytecode
                                                   output file
-  --eei-checks                                    run EEI compatibility checks (default: False)
-  --skip-eei-checks                               deprecated flag
-  --ignore-eei-checks                             deprecated flag
 
 ```
 ## Group **Transactions**
@@ -525,7 +517,7 @@ Output example:
     "emittedTransactionHash": "the transaction hash"
 }
 
-optional arguments:
+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)
@@ -594,7 +586,7 @@ Output example:
     "emittedTransactionHash": "the transaction hash"
 }
 
-optional arguments:
+options:
   -h, --help         show this help message and exit
   --infile INFILE    input file (a previously saved transaction)
   --outfile OUTFILE  where to save the output (the hash) (default: stdout)
@@ -621,7 +613,7 @@ Output example:
     }
 }
 
-optional arguments:
+options:
   -h, --help                 show this help message and exit
   --hash HASH                the hash
   --sender SENDER            the sender address
@@ -655,7 +647,7 @@ usage: mxpy hyperblock get [-h] ...
 
 Get hyperblock
 
-optional arguments:
+options:
   -h, --help     show this help message and exit
   --proxy PROXY  🔗 the URL of the proxy (default: https://testnet-gateway.multiversx.com)
   --key KEY      the hash or the nonce of the hyperblock
@@ -702,7 +694,7 @@ usage: mxpy validator stake [-h] ...
 
 Stake value into the Network
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --proxy PROXY                                   🔗 the URL of the proxy (default: https://testnet-
                                                   gateway.multiversx.com)
@@ -753,7 +745,7 @@ usage: mxpy validator unstake [-h] ...
 
 Unstake value
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --proxy PROXY                                   🔗 the URL of the proxy (default: https://testnet-
                                                   gateway.multiversx.com)
@@ -802,7 +794,7 @@ usage: mxpy validator unjail [-h] ...
 
 Unjail a Validator Node
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --proxy PROXY                                   🔗 the URL of the proxy (default: https://testnet-
                                                   gateway.multiversx.com)
@@ -851,7 +843,7 @@ usage: mxpy validator unbond [-h] ...
 
 Unbond tokens for a bls key
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --proxy PROXY                                   🔗 the URL of the proxy (default: https://testnet-
                                                   gateway.multiversx.com)
@@ -900,7 +892,7 @@ usage: mxpy validator change-reward-address [-h] ...
 
 Change the reward address
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --proxy PROXY                                   🔗 the URL of the proxy (default: https://testnet-
                                                   gateway.multiversx.com)
@@ -949,7 +941,7 @@ usage: mxpy validator claim [-h] ...
 
 Claim rewards
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --proxy PROXY                                   🔗 the URL of the proxy (default: https://testnet-
                                                   gateway.multiversx.com)
@@ -998,16 +990,15 @@ usage: mxpy account COMMAND [-h] ...
 Get Account data (nonce, balance) from the Network
 
 COMMANDS:
-  {get,get-transactions}
+  {get}
 
 OPTIONS:
-  -h, --help            show this help message and exit
+  -h, --help  show this help message and exit
 
 ----------------
 COMMANDS summary
 ----------------
 get                            Query account details (nonce, balance etc.)
-get-transactions               Query account transactions
 
 ```
 ### Account.Get
@@ -1019,7 +1010,7 @@ usage: mxpy account get [-h] ...
 
 Query account details (nonce, balance etc.)
 
-optional arguments:
+options:
   -h, --help                 show this help message and exit
   --proxy PROXY              🔗 the URL of the proxy (default: https://testnet-gateway.multiversx.com)
   --address ADDRESS          🖄 the address to query
@@ -1028,22 +1019,6 @@ optional arguments:
   --username                 whether to only fetch the username
   --omit-fields OMIT_FIELDS  omit fields in the output payload (default: [])
 
-```
-### Account.GetTransactions
-
-
-```
-$ mxpy account get-transactions --help
-usage: mxpy account get-transactions [-h] ...
-
-Query account transactions
-
-optional arguments:
-  -h, --help         show this help message and exit
-  --proxy PROXY      🔗 the URL of the proxy (default: https://testnet-gateway.multiversx.com)
-  --outfile OUTFILE  where to save the output (default: stdout)
-  --address ADDRESS  🖄 the address to query
-
 ```
 ## Group **Wallet**
 
@@ -1055,7 +1030,7 @@ usage: mxpy wallet COMMAND [-h] ...
 Create wallet, derive secret key from mnemonic, bech32 address helpers etc.
 
 COMMANDS:
-  {new,derive,convert,bech32,pem-address,pem-address-hex}
+  {new,convert,bech32,pem-address,pem-address-hex}
 
 OPTIONS:
   -h, --help            show this help message and exit
@@ -1064,7 +1039,6 @@ OPTIONS:
 COMMANDS summary
 ----------------
 new                            Create a new wallet and print its mnemonic; optionally save as password-protected JSON (recommended) or PEM (not recommended)
-derive                         DEPRECATED COMMAND, replaced by 'wallet convert'
 convert                        Convert a wallet from one format to another
 bech32                         Helper for encoding and decoding bech32 addresses
 pem-address                    Get the public address out of a PEM file as bech32
@@ -1080,11 +1054,8 @@ usage: mxpy wallet new [-h] ...
 
 Create a new wallet and print its mnemonic; optionally save as password-protected JSON (recommended) or PEM (not recommended)
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
-  --json                                          DEPRECATED, replaced by --format=keystore-mnemonic
-  --pem                                           DEPRECATED, replaced by --format=pem
-  --output-path OUTPUT_PATH                       DEPRECATED, replaced by --outfile
   --format {raw-mnemonic,keystore-mnemonic,keystore-secret-key,pem}
                                                   the format of the generated wallet file (default: None)
   --outfile OUTFILE                               the output path and base file name for the generated wallet files
@@ -1092,24 +1063,6 @@ optional arguments:
   --address-hrp ADDRESS_HRP                       the human-readable part of the address, when format is keystore-
                                                   secret-key or pem (default: erd)
 
-```
-### Wallet.Derive
-
-
-```
-$ mxpy wallet derive --help
-usage: mxpy wallet derive [-h] ...
-
-DEPRECATED COMMAND, replaced by 'wallet convert'
-
-positional arguments:
-  pem            path of the output PEM file
-
-optional arguments:
-  -h, --help     show this help message and exit
-  --mnemonic     whether to derive from an existing mnemonic
-  --index INDEX  the address index
-
 ```
 ### Wallet.Convert
 
@@ -1120,7 +1073,7 @@ usage: mxpy wallet convert [-h] ...
 
 Convert a wallet from one format to another
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --infile INFILE                                 path to the input file
   --outfile OUTFILE                               path to the output file
@@ -1147,82 +1100,127 @@ Helper for encoding and decoding bech32 addresses
 positional arguments:
   value       the value to encode or decode
 
-optional arguments:
+options:
   -h, --help  show this help message and exit
   --encode    whether to encode
   --decode    whether to decode
 
 ```
-## Group **Testnet**
+## Group **Localnet**
 
 
 ```
-$ mxpy testnet --help
-usage: mxpy testnet COMMAND [-h] ...
+$ mxpy localnet --help
+usage: mxpy localnet COMMAND [-h] ...
 
-Set up, start and control local testnets
+Set up, start and control localnets
 
 COMMANDS:
-  {prerequisites,start,config,clean}
+  {setup,new,prerequisites,build,start,config,clean}
 
 OPTIONS:
   -h, --help            show this help message and exit
 
 ```
-### Testnet.Prerequisites
+### Localnet.Setup
 
 
 ```
-$ mxpy testnet prerequisites --help
-usage: mxpy testnet prerequisites [-h] ...
+$ mxpy localnet setup --help
+usage: mxpy localnet setup [-h] ...
 
-Download and verify the prerequisites for running a testnet
+Set up a localnet (runs 'prerequisites', 'build' and 'config' in one go)
 
-optional arguments:
+options:
   -h, --help               show this help message and exit
-  --configfile CONFIGFILE  An optional configuration file describing the testnet
+  --configfile CONFIGFILE  An optional configuration file describing the localnet
 
 ```
-### Testnet.Config
+### Localnet.New
 
 
 ```
-$ mxpy testnet config --help
-usage: mxpy testnet config [-h] ...
+$ mxpy localnet new --help
+usage: mxpy localnet new [-h] ...
 
-Configure a testnet (required before starting it the first time or after clean)
+Create a new localnet configuration
 
-optional arguments:
+options:
   -h, --help               show this help message and exit
-  --configfile CONFIGFILE  An optional configuration file describing the testnet
+  --configfile CONFIGFILE  An optional configuration file describing the localnet
 
 ```
-### Testnet.Start
+### Localnet.Prerequisites
 
 
 ```
-$ mxpy testnet start --help
-usage: mxpy testnet start [-h] ...
+$ mxpy localnet prerequisites --help
+usage: mxpy localnet prerequisites [-h] ...
 
-Start a testnet
+Download and verify the prerequisites for running a localnet
 
-optional arguments:
+options:
   -h, --help               show this help message and exit
-  --configfile CONFIGFILE  An optional configuration file describing the testnet
+  --configfile CONFIGFILE  An optional configuration file describing the localnet
+
+```
+### Localnet.Build
+
+
+```
+$ mxpy localnet build --help
+usage: mxpy localnet build [-h] ...
+
+Build necessary software for running a localnet
+
+options:
+  -h, --help                                      show this help message and exit
+  --configfile CONFIGFILE                         An optional configuration file describing the localnet
+  --software {node,seednode,proxy} [{node,seednode,proxy} ...]
+                                                  The software to build (default: ['node', 'seednode', 'proxy'])
+
+```
+### Localnet.Config
+
+
+```
+$ mxpy localnet config --help
+usage: mxpy localnet config [-h] ...
+
+Configure a localnet (required before starting it the first time or after clean)
+
+options:
+  -h, --help               show this help message and exit
+  --configfile CONFIGFILE  An optional configuration file describing the localnet
+
+```
+### Localnet.Start
+
+
+```
+$ mxpy localnet start --help
+usage: mxpy localnet start [-h] ...
+
+Start a localnet
+
+options:
+  -h, --help                               show this help message and exit
+  --configfile CONFIGFILE                  An optional configuration file describing the localnet
+  --stop-after-seconds STOP_AFTER_SECONDS  Stop the localnet after a given number of seconds (default: 31536000)
 
 ```
-### Testnet.Clean
+### Localnet.Clean
 
 
 ```
-$ mxpy testnet clean --help
-usage: mxpy testnet clean [-h] ...
+$ mxpy localnet clean --help
+usage: mxpy localnet clean [-h] ...
 
-Erase the currently configured testnet (must be already stopped)
+Erase the currently configured localnet (must be already stopped)
 
-optional arguments:
+options:
   -h, --help               show this help message and exit
-  --configfile CONFIGFILE  An optional configuration file describing the testnet
+  --configfile CONFIGFILE  An optional configuration file describing the localnet
 
 ```
 ## Group **Network**
@@ -1257,7 +1255,7 @@ usage: mxpy network num-shards [-h] ...
 
 Get the number of shards.
 
-optional arguments:
+options:
   -h, --help     show this help message and exit
   --proxy PROXY  🔗 the URL of the proxy (default: https://testnet-gateway.multiversx.com)
 
@@ -1271,7 +1269,7 @@ usage: mxpy network block-nonce [-h] ...
 
 Get the latest block nonce, by shard.
 
-optional arguments:
+options:
   -h, --help     show this help message and exit
   --proxy PROXY  🔗 the URL of the proxy (default: https://testnet-gateway.multiversx.com)
   --shard SHARD  the shard ID (use 4294967295 for metachain)
@@ -1286,7 +1284,7 @@ usage: mxpy network chain [-h] ...
 
 Get the chain identifier.
 
-optional arguments:
+options:
   -h, --help     show this help message and exit
   --proxy PROXY  🔗 the URL of the proxy (default: https://testnet-gateway.multiversx.com)
 
@@ -1326,7 +1324,7 @@ positional arguments:
   {all,llvm,clang,cpp,rust,golang,vmtools,mx_chain_go,mx_chain_proxy_go,wasm-opt,twiggy,testwallets}
                                                   the dependency to install
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --overwrite                                     whether to overwrite an existing installation
   --tag TAG                                       the tag or version to install
@@ -1345,7 +1343,7 @@ positional arguments:
   {all,llvm,clang,cpp,rust,golang,vmtools,mx_chain_go,mx_chain_proxy_go,wasm-opt,twiggy,testwallets}
                                                   the dependency to check
 
-optional arguments:
+options:
   -h, --help                                      show this help message and exit
   --tag TAG                                       the tag or version to check
 
@@ -1386,7 +1384,7 @@ usage: mxpy config dump [-h] ...
 
 Dumps configuration.
 
-optional arguments:
+options:
   -h, --help  show this help message and exit
   --defaults  dump defaults instead of local config
 
@@ -1403,7 +1401,7 @@ Gets a configuration value.
 positional arguments:
   name        the name of the configuration entry
 
-optional arguments:
+options:
   -h, --help  show this help message and exit
 
 ```
@@ -1420,7 +1418,7 @@ positional arguments:
   name        the name of the configuration entry
   value       the new value
 
-optional arguments:
+options:
   -h, --help  show this help message and exit
 
 ```
@@ -1436,7 +1434,7 @@ Creates a new configuration.
 positional arguments:
   name                 the name of the configuration entry
 
-optional arguments:
+options:
   -h, --help           show this help message and exit
   --template TEMPLATE  template from which to create the new config
 
@@ -1453,7 +1451,7 @@ Switch to a different config
 positional arguments:
   name        the name of the configuration entry
 
-optional arguments:
+options:
   -h, --help  show this help message and exit
 
 ```
@@ -1466,7 +1464,7 @@ usage: mxpy config list [-h] ...
 
 List available configs
 
-optional arguments:
+options:
   -h, --help  show this help message and exit
 
 ```
@@ -1502,7 +1500,7 @@ usage: mxpy data parse [-h] ...
 
 Parses values from a given file
 
-optional arguments:
+options:
   -h, --help               show this help message and exit
   --file FILE              path of the file to parse
   --expression EXPRESSION  the Python-Dictionary expression to evaluate in order to extract the data
@@ -1517,7 +1515,7 @@ usage: mxpy data store [-h] ...
 
 Stores a key-value pair within a partition
 
-optional arguments:
+options:
   -h, --help             show this help message and exit
   --key KEY              the key
   --value VALUE          the value to save
@@ -1534,7 +1532,7 @@ usage: mxpy data load [-h] ...
 
 Loads a key-value pair from a storage partition
 
-optional arguments:
+options:
   -h, --help             show this help message and exit
   --key KEY              the key
   --partition PARTITION  the storage partition (default: *)
diff --git a/CLI.md.sh b/CLI.md.sh
index 387f9ef3..4017b726 100755
--- a/CLI.md.sh
+++ b/CLI.md.sh
@@ -68,19 +68,20 @@ generate() {
 
     group "Account" "account"
     command "Account.Get" "account get"
-    command "Account.GetTransactions" "account get-transactions"
 
     group "Wallet" "wallet"
     command "Wallet.New" "wallet new"
-    command "Wallet.Derive" "wallet derive"
     command "Wallet.Convert" "wallet convert"
     command "Wallet.Bech32" "wallet bech32"
 
-    group "Testnet" "testnet"
-    command "Testnet.Prerequisites" "testnet prerequisites"
-    command "Testnet.Config" "testnet config"
-    command "Testnet.Start" "testnet start"
-    command "Testnet.Clean" "testnet clean"
+    group "Localnet" "localnet"
+    command "Localnet.Setup" "localnet setup"
+    command "Localnet.New" "localnet new"
+    command "Localnet.Prerequisites" "localnet prerequisites"
+    command "Localnet.Build" "localnet build"
+    command "Localnet.Config" "localnet config"
+    command "Localnet.Start" "localnet start"
+    command "Localnet.Clean" "localnet clean"
 
     group "Network" "network"
     command "Network.NumShards" "network num-shards"
diff --git a/multiversx_sdk_cli/cli.py b/multiversx_sdk_cli/cli.py
index c99eb033..a4db72d5 100644
--- a/multiversx_sdk_cli/cli.py
+++ b/multiversx_sdk_cli/cli.py
@@ -13,13 +13,13 @@
 import multiversx_sdk_cli.cli_deps
 import multiversx_sdk_cli.cli_dns
 import multiversx_sdk_cli.cli_ledger
+import multiversx_sdk_cli.cli_localnet
 import multiversx_sdk_cli.cli_network
-import multiversx_sdk_cli.cli_testnet
 import multiversx_sdk_cli.cli_transactions
 import multiversx_sdk_cli.cli_validators
 import multiversx_sdk_cli.cli_wallet
 import multiversx_sdk_cli.version
-from multiversx_sdk_cli import config, errors
+from multiversx_sdk_cli import config, errors, ux
 
 logger = logging.getLogger("cli")
 
@@ -29,6 +29,7 @@ def main(cli_args: List[str] = sys.argv[1:]):
         _do_main(cli_args)
     except errors.KnownError as err:
         logger.critical(err.get_pretty())
+        ux.show_critical_error(err.get_pretty())
         return 1
     except KeyboardInterrupt:
         print("process killed by user.")
@@ -90,7 +91,7 @@ def setup_parser(args: List[str]):
     commands.append(multiversx_sdk_cli.cli_deps.setup_parser(subparsers))
     commands.append(multiversx_sdk_cli.cli_config.setup_parser(subparsers))
     commands.append(multiversx_sdk_cli.cli_block.setup_parser(subparsers))
-    commands.append(multiversx_sdk_cli.cli_testnet.setup_parser(args, subparsers))
+    commands.append(multiversx_sdk_cli.cli_localnet.setup_parser(args, subparsers))
     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))
diff --git a/multiversx_sdk_cli/cli_accounts.py b/multiversx_sdk_cli/cli_accounts.py
index 7df3c27b..db0882a1 100644
--- a/multiversx_sdk_cli/cli_accounts.py
+++ b/multiversx_sdk_cli/cli_accounts.py
@@ -1,9 +1,11 @@
 import logging
 from typing import Any
 
+from multiversx_sdk_network_providers.proxy_network_provider import \
+    ProxyNetworkProvider
+
 from multiversx_sdk_cli import cli_shared, utils
 from multiversx_sdk_cli.accounts import Address
-from multiversx_sdk_network_providers.proxy_network_provider import ProxyNetworkProvider
 
 logger = logging.getLogger("cli.accounts")
 
@@ -22,12 +24,6 @@ def setup_parser(subparsers: Any) -> Any:
     cli_shared.add_omit_fields_arg(sub)
     sub.set_defaults(func=get_account)
 
-    sub = cli_shared.add_command_subparser(subparsers, "account", "get-transactions", "Query account transactions")
-    cli_shared.add_proxy_arg(sub)
-    cli_shared.add_outfile_arg(sub)
-    _add_address_arg(sub)
-    sub.set_defaults(func=get_account_transactions)
-
     parser.epilog = cli_shared.build_group_epilog(subparsers)
     return subparsers
 
@@ -50,13 +46,3 @@ def get_account(args: Any):
         print(account.username)
     else:
         utils.dump_out_json(account.to_dictionary())
-
-
-def get_account_transactions(args: Any):
-    proxy_url = args.proxy
-    address = args.address
-    proxy = ProxyNetworkProvider(proxy_url)
-
-    response = proxy.get_account_transactions(Address(address))
-    transactions_as_dictionaries = [tx.to_dictionary() for tx in response]
-    utils.dump_out_json(transactions_as_dictionaries, args.outfile)
diff --git a/multiversx_sdk_cli/cli_contracts.py b/multiversx_sdk_cli/cli_contracts.py
index 2288e053..bbf885bb 100644
--- a/multiversx_sdk_cli/cli_contracts.py
+++ b/multiversx_sdk_cli/cli_contracts.py
@@ -15,7 +15,6 @@
 from multiversx_sdk_cli.contracts import CodeMetadata, SmartContract
 from multiversx_sdk_cli.docker import is_docker_installed, run_docker
 from multiversx_sdk_cli.errors import DockerMissingError, NoWalletProvided
-from multiversx_sdk_cli.projects import load_project
 from multiversx_sdk_cli.projects.core import get_project_paths_recursively
 from multiversx_sdk_cli.transactions import Transaction
 from multiversx_sdk_cli.cosign_transaction import cosign_transaction
@@ -71,7 +70,7 @@ 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_project_or_bytecode_arg(sub)
+    _add_bytecode_arg(sub)
     _add_metadata_arg(sub)
     cli_shared.add_outfile_arg(sub)
     cli_shared.add_wallet_args(args, sub)
@@ -109,7 +108,7 @@ def setup_parser(args: List[str], subparsers: Any) -> Any:
                                            f"Upgrade a previously-deployed Smart Contract.{output_description}")
     _add_contract_arg(sub)
     cli_shared.add_outfile_arg(sub)
-    _add_project_or_bytecode_arg(sub)
+    _add_bytecode_arg(sub)
     _add_metadata_arg(sub)
     cli_shared.add_wallet_args(args, sub)
     cli_shared.add_proxy_arg(sub)
@@ -190,22 +189,14 @@ def _add_build_options_args(sub: Any):
     sub.add_argument("--wasm-suffix", type=str,
                      help="for rust projects, optionally specify the suffix of the wasm bytecode output file")
 
-    sub.add_argument("--eei-checks", action="store_true", default=False, help="run EEI compatibility checks (default: %(default)s)")
-    # Flags are kept in order to avoid breaking changes, for the moment - we might completely remove them in the future.
-    sub.add_argument("--skip-eei-checks", action="store_true", default=True, help="deprecated flag")
-    sub.add_argument("--ignore-eei-checks", action="store_true", default=True, help="deprecated flag")
-
 
 def _add_recursive_arg(sub: Any):
     sub.add_argument("-r", "--recursive", dest="recursive", action="store_true", help="locate projects recursively")
 
 
-def _add_project_or_bytecode_arg(sub: Any):
-    group = sub.add_mutually_exclusive_group(required=True)
-    group.add_argument("--project", default=os.getcwd(),
-                       help="🗀 the project directory (default: current directory)")
-    group.add_argument("--bytecode", type=str,
-                       help="the file containing the WASM bytecode")
+def _add_bytecode_arg(sub: Any):
+    sub.add_argument("--bytecode", type=str, required=True,
+                     help="the file containing the WASM bytecode")
 
 
 def _add_contract_arg(sub: Any):
@@ -277,12 +268,7 @@ def _prepare_build_options(args: Any) -> Dict[str, Any]:
         "cargo-target-dir": args.cargo_target_dir,
         "wasm-symbols": args.wasm_symbols,
         "wasm-name": args.wasm_name,
-        "wasm-suffix": args.wasm_suffix,
-        "eei-checks": args.eei_checks,
-        # TODO: Remove this, in the future
-        "skip-eei-checks": args.skip_eei_checks,
-        # TODO: Remove this, in the future
-        "ignore-eei-checks": args.ignore_eei_checks
+        "wasm-suffix": args.wasm_suffix
     }
 
 
@@ -322,12 +308,7 @@ def deploy(args: Any):
 
 
 def _prepare_contract(args: Any) -> SmartContract:
-    if args.bytecode and len(args.bytecode):
-        bytecode = utils.read_binary_file(Path(args.bytecode)).hex()
-    else:
-        project_path = Path(args.project)
-        project = load_project(project_path)
-        bytecode = project.get_bytecode()
+    bytecode = utils.read_binary_file(Path(args.bytecode)).hex()
 
     metadata = CodeMetadata(upgradeable=args.metadata_upgradeable, readable=args.metadata_readable,
                             payable=args.metadata_payable, payable_by_sc=args.metadata_payable_by_sc)
diff --git a/multiversx_sdk_cli/cli_localnet.py b/multiversx_sdk_cli/cli_localnet.py
new file mode 100644
index 00000000..ce62507c
--- /dev/null
+++ b/multiversx_sdk_cli/cli_localnet.py
@@ -0,0 +1,173 @@
+import logging
+from pathlib import Path
+from typing import Any, List
+
+from multiversx_sdk_cli import cli_shared, ux
+from multiversx_sdk_cli.constants import ONE_YEAR_IN_SECONDS
+from multiversx_sdk_cli.errors import KnownError
+from multiversx_sdk_cli.localnet import (step_build_software, step_clean,
+                                         step_config, step_new,
+                                         step_prerequisites, step_start)
+
+logger = logging.getLogger("cli.localnet")
+
+
+def setup_parser(args: List[str], subparsers: Any) -> Any:
+    parser = cli_shared.add_group_subparser(
+        subparsers,
+        "localnet",
+        "Set up, start and control localnets"
+    )
+    subparsers = parser.add_subparsers()
+
+    # Setup
+    sub = cli_shared.add_command_subparser(
+        subparsers,
+        "localnet",
+        "setup",
+        "Set up a localnet (runs 'prerequisites', 'build' and 'config' in one go)"
+    )
+    add_argument_configfile(sub)
+    sub.set_defaults(func=localnet_setup)
+
+    # New
+    sub = cli_shared.add_command_subparser(
+        subparsers,
+        "localnet",
+        "new",
+        "Create a new localnet configuration"
+    )
+    add_argument_configfile(sub)
+    sub.set_defaults(func=localnet_new)
+
+    # Prerequisites
+    sub = cli_shared.add_command_subparser(
+        subparsers,
+        "localnet",
+        "prerequisites",
+        "Download and verify the prerequisites for running a localnet"
+    )
+    add_argument_configfile(sub)
+    sub.set_defaults(func=localnet_prerequisites)
+
+    # Build
+    sub = cli_shared.add_command_subparser(
+        subparsers,
+        "localnet",
+        "build",
+        "Build necessary software for running a localnet"
+    )
+    add_argument_configfile(sub)
+    sub.add_argument('--software', choices=["node", "seednode", "proxy"], nargs="+", default=["node", "seednode", "proxy"], help="The software to build (default: %(default)s)")
+    sub.set_defaults(func=localnet_build)
+
+    # Start
+    sub = cli_shared.add_command_subparser(
+        subparsers,
+        "localnet",
+        "start",
+        "Start a localnet"
+    )
+    add_argument_configfile(sub)
+    sub.add_argument("--stop-after-seconds", type=int, required=False, default=ONE_YEAR_IN_SECONDS, help="Stop the localnet after a given number of seconds (default: %(default)s)")
+    sub.set_defaults(func=localnet_start)
+
+    # Config
+    sub = cli_shared.add_command_subparser(
+        subparsers,
+        "localnet",
+        "config",
+        "Configure a localnet (required before starting it the first time or after clean)"
+    )
+    add_argument_configfile(sub)
+    sub.set_defaults(func=localnet_config)
+
+    # Clean
+    sub = cli_shared.add_command_subparser(
+        subparsers,
+        "localnet",
+        "clean",
+        "Erase the currently configured localnet (must be already stopped)"
+    )
+    add_argument_configfile(sub)
+    sub.set_defaults(func=localnet_clean)
+
+
+def add_argument_configfile(parser: Any):
+    help_config_file = "An optional configuration file describing the localnet"
+    parser.add_argument("--configfile", type=Path, required=False, default=Path("localnet.toml"), help=help_config_file)
+
+
+def localnet_new(args: Any):
+    logger.info("New localnet (creating configuration file)...")
+
+    step_new.new_config(args.configfile)
+
+    ux.show_message("New localnet configuration file created (or already existing). Make sure to inspect it. In order to fetch localnet prerequisites, run:\n\n$ mxpy localnet prerequisites")
+
+
+def localnet_clean(args: Any):
+    logger.info("Cleaning localnet...")
+    guard_configfile(args)
+
+    step_clean.clean(configfile=args.configfile)
+
+    ux.show_message("Localnet cleaned. In order to configure (prepare) the localnet, run:\n\n$ mxpy localnet config")
+
+
+def localnet_prerequisites(args: Any):
+    logger.info("Gathering prerequisites...")
+    guard_configfile(args)
+
+    step_prerequisites.fetch_prerequisites(configfile=args.configfile)
+
+    ux.show_message("Prerequisites gathered. In order to build the localnet software, run:\n\n$ mxpy localnet build")
+
+
+def localnet_build(args: Any):
+    logger.info("Building binaries...")
+    guard_configfile(args)
+
+    step_build_software.build(configfile=args.configfile, software_components=args.software)
+
+    ux.show_message("Binaries built. In order to configure (prepare) the localnet, run:\n\n$ mxpy localnet config")
+
+
+def localnet_config(args: Any):
+    logger.info("Configuring localnet...")
+    guard_configfile(args)
+
+    step_config.configure(configfile=args.configfile)
+
+    ux.show_message("Localnet configured. In order to start it, run:\n\n$ mxpy localnet start")
+
+
+def localnet_start(args: Any):
+    logger.info("Starting localnet...")
+    guard_configfile(args)
+
+    step_start.start(configfile=args.configfile, stop_after_seconds=args.stop_after_seconds)
+
+
+def localnet_setup(args: Any):
+    logger.info("Setting up localnet...")
+
+    step_new.new_config(args.configfile)
+    step_prerequisites.fetch_prerequisites(configfile=args.configfile)
+    step_build_software.build(configfile=args.configfile, software_components=["node", "seednode", "proxy"])
+    step_clean.clean(configfile=args.configfile)
+    step_config.configure(configfile=args.configfile)
+
+    ux.show_message("Localnet setup complete. In order to start it, run:\n\n$ mxpy localnet start")
+
+
+def guard_configfile(args: Any):
+    configfile = args.configfile.resolve()
+    old_configfile_in_workdir = Path("testnet.toml").resolve()
+
+    if not configfile.exists():
+        raise KnownError(f"Localnet config file does not exist: {configfile}")
+
+    if old_configfile_in_workdir.exists():
+        logger.error(f"""For less ambiguity, the old "testnet.toml" config file should be removed: {old_configfile_in_workdir}""")
+        raise KnownError(f"""Found old "testnet.toml" config file in working directory: {old_configfile_in_workdir}""")
diff --git a/multiversx_sdk_cli/cli_wallet.py b/multiversx_sdk_cli/cli_wallet.py
index d1f02dbe..09d6ca03 100644
--- a/multiversx_sdk_cli/cli_wallet.py
+++ b/multiversx_sdk_cli/cli_wallet.py
@@ -41,32 +41,17 @@ def setup_parser(args: List[str], subparsers: Any) -> Any:
         "new",
         "Create a new wallet and print its mnemonic; optionally save as password-protected JSON (recommended) or PEM (not recommended)"
     )
-    sub.add_argument("--json", help="DEPRECATED, replaced by --format=keystore-mnemonic", action="store_true", default=False)
-    sub.add_argument("--pem", help="DEPRECATED, replaced by --format=pem", action="store_true", default=False)
-    sub.add_argument("--output-path", help="DEPRECATED, replaced by --outfile", type=str)
     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="erd")
     sub.set_defaults(func=wallet_new)
 
-    sub = cli_shared.add_command_subparser(
-        subparsers,
-        "wallet",
-        "derive",
-        "DEPRECATED COMMAND, replaced by 'wallet convert'"
-    )
-    sub.add_argument("pem", help="path of the output PEM file")
-    sub.add_argument("--mnemonic", action="store_true", help="whether to derive from an existing mnemonic")
-    sub.add_argument("--index", help="the address index", type=int, default=0)
-    sub.set_defaults(func=wallet_derive_deprecated)
-
     sub = cli_shared.add_command_subparser(
         subparsers,
         "wallet",
         "convert",
         "Convert a wallet from one format to another"
     )
-
     sub.add_argument("--infile", help="path to the input file")
     sub.add_argument("--outfile", help="path to the output file")
     sub.add_argument("--in-format", required=True, choices=WALLET_FORMATS, help="the format of the input file")
@@ -116,17 +101,6 @@ def wallet_new(args: Any):
     outfile = args.outfile
     address_hrp = args.address_hrp
 
-    # Handle deprecated options
-    if args.json:
-        logger.warning("The --json option is deprecated, use --format=keystore-mnemonic instead.")
-        format = "keystore-mnemonic"
-    if args.pem:
-        logger.warning("The --pem option is deprecated, use --format=pem instead.")
-        format = "pem"
-    if args.output_path:
-        logger.warning("The --output-path option is deprecated, use --outfile instead.")
-        outfile = args.output_path
-
     mnemonic = Mnemonic.generate()
     print(f"Mnemonic: {mnemonic.get_text()}")
 
@@ -257,31 +231,6 @@ def _create_wallet_content(
     raise KnownError(f"Cannot create wallet, unknown output format: <{out_format}>. Make sure to use one of following: {WALLET_FORMATS}.")
 
 
-def wallet_derive_deprecated(args: Any):
-    logger.warning("This command is deprecated. Use 'wallet convert' instead.")
-
-    pem_file_path = Path(args.pem)
-    ask_mnemonic = args.mnemonic
-    index = args.index
-
-    if ask_mnemonic:
-        mnemonic_str = input("Enter mnemonic:\n").rstrip().replace("\n", "")
-        mnemonic = Mnemonic(mnemonic_str)
-        secret_key = mnemonic.derive_key(index)
-        pubkey = secret_key.generate_public_key()
-    else:
-        mnemonic = Mnemonic.generate()
-
-    secret_key = mnemonic.derive_key(index)
-    pubkey = secret_key.generate_public_key()
-    address = Address(pubkey.hex())
-
-    pem_file = UserPEM(address.bech32(), secret_key)
-    pem_file.save(pem_file_path)
-
-    logger.info(f"Created PEM file [{pem_file_path}] for [{address.bech32()}]")
-
-
 def do_bech32(args: Any):
     encode = args.encode
     value = args.value
diff --git a/multiversx_sdk_cli/config.py b/multiversx_sdk_cli/config.py
index 4587439a..02260a56 100644
--- a/multiversx_sdk_cli/config.py
+++ b/multiversx_sdk_cli/config.py
@@ -170,13 +170,6 @@ def get_defaults() -> Dict[str, Any]:
         "dependencies.llvm.urlTemplate.osx": "https://ide.elrond.com/vendor-llvm/{TAG}/darwin-amd64.tar.gz?t=19feb",
         "dependencies.rust.resolution": "SDK",
         "dependencies.rust.tag": "nightly",
-        "dependencies.mx_chain_go.tag": "latest",
-        "dependencies.mx_chain_go.urlTemplate.linux": "https://github.com/multiversx/mx-chain-go/archive/{TAG}.tar.gz",
-        "dependencies.mx_chain_go.urlTemplate.osx": "https://github.com/multiversx/mx-chain-go/archive/{TAG}.tar.gz",
-        "dependencies.mx_chain_go.url": "https://github.com/multiversx/mx-chain-go/archive/{TAG}.tar.gz",
-        "dependencies.mx_chain_proxy_go.tag": "latest",
-        "dependencies.mx_chain_proxy_go.urlTemplate.linux": "https://github.com/multiversx/mx-chain-proxy-go/archive/{TAG}.tar.gz",
-        "dependencies.mx_chain_proxy_go.urlTemplate.osx": "https://github.com/multiversx/mx-chain-proxy-go/archive/{TAG}.tar.gz",
         "dependencies.golang.resolution": "SDK",
         "dependencies.golang.tag": "go1.18.4",
         "dependencies.golang.urlTemplate.linux": "https://golang.org/dl/{TAG}.linux-amd64.tar.gz",
@@ -193,7 +186,6 @@ def get_defaults() -> Dict[str, Any]:
         "dependencies.wasm-opt.urlTemplate.linux": "https://github.com/WebAssembly/binaryen/releases/download/{TAG}/binaryen-{TAG}-x86_64-linux.tar.gz",
         "dependencies.wasm-opt.urlTemplate.osx": "https://github.com/WebAssembly/binaryen/releases/download/{TAG}/binaryen-{TAG}-x86_64-macos.tar.gz",
         "dependencies.wasm-opt.urlTemplate.windows": "https://github.com/WebAssembly/binaryen/releases/download/{TAG}/binaryen-{TAG}-x86_64-windows.tar.gz",
-        "testnet.validate_expected_keys": "false",
         "github_api_token": "",
     }
 
diff --git a/multiversx_sdk_cli/constants.py b/multiversx_sdk_cli/constants.py
index 3ba6b3ea..bdbf2cf3 100644
--- a/multiversx_sdk_cli/constants.py
+++ b/multiversx_sdk_cli/constants.py
@@ -5,3 +5,4 @@
 SC_HEX_PUBKEY_PREFIX_WASM_VM = SC_HEX_PUBKEY_PREFIX + VM_TYPE_WASM_VM
 DEFAULT_CARGO_TARGET_DIR_NAME = "default_cargo_target"
 TRANSACTION_OPTIONS_TX_GUARDED = 0b0010
+ONE_YEAR_IN_SECONDS = 60 * 60 * 24 * 365
diff --git a/multiversx_sdk_cli/dependencies/modules.py b/multiversx_sdk_cli/dependencies/modules.py
index 929808e1..6b013569 100644
--- a/multiversx_sdk_cli/dependencies/modules.py
+++ b/multiversx_sdk_cli/dependencies/modules.py
@@ -28,10 +28,10 @@ def install(self, tag: str, overwrite: bool) -> None:
         if tag == 'latest':
             tag = self.get_latest_release()
 
-        logger.debug(f"install: key={self.key}, tag={tag}")
+        logger.info(f"install: key={self.key}, tag={tag}, overwrite={overwrite}")
 
         if self._should_skip(tag, overwrite):
-            logger.debug("Already exists. Skip install.")
+            logger.info("Already exists. Skip install.")
             return
 
         self._guard_cannot_install_on_host()
@@ -233,18 +233,23 @@ def get_env(self) -> Dict[str, str]:
                 "GOROOT": os.environ.get("GOROOT", "")
             }
         if resolution == DependencyResolution.SDK:
+            current_path = os.environ.get("PATH", "")
+            current_path_parts = current_path.split(":")
+            current_path_parts_without_go = [part for part in current_path_parts if "/go/bin" not in part]
+            current_path_without_go = ":".join(current_path_parts_without_go)
+
             return {
                 # At this moment, cc (build-essential) is needed to compile go dependencies (e.g. Node, VM)
-                "PATH": f"{(directory / 'go' / 'bin')}:{os.environ['PATH']}",
-                "GOPATH": self.get_gopath(),
+                "PATH": f"{(directory / 'go' / 'bin')}:{current_path_without_go}",
+                "GOPATH": str(self.get_gopath()),
                 "GOCACHE": str(parent_directory / "GOCACHE"),
                 "GOROOT": str(directory / "go")
             }
 
         raise errors.BadDependencyResolution(self.key, resolution)
 
-    def get_gopath(self):
-        return path.join(self.get_parent_directory(), "GOPATH")
+    def get_gopath(self) -> Path:
+        return self.get_parent_directory() / "GOPATH"
 
     def get_latest_release(self) -> str:
         raise errors.UnsupportedConfigurationValue("Golang tag must always be explicit, not latest")
@@ -268,17 +273,14 @@ def _do_install(self, tag: str) -> None:
         myprocess.run_process(args, env=self.get_env_for_install())
 
     def _get_installer_url(self) -> str:
-        platform = workstation.get_platform()
-
-        if platform == "windows":
+        if workstation.is_windows():
             return "https://win.rustup.rs"
         return "https://sh.rustup.rs"
 
     def _get_installer_path(self) -> Path:
-        platform = workstation.get_platform()
         tools_folder = workstation.get_tools_folder()
 
-        if platform == "windows":
+        if workstation.is_windows():
             return tools_folder / "rustup-init.exe"
         return tools_folder / "rustup.sh"
 
@@ -342,7 +344,6 @@ def _get_env_for_is_installed_in_sdk(self) -> Dict[str, str]:
 
     def get_env_for_install(self):
         directory = self.get_directory("")
-        platform = workstation.get_platform()
 
         env = {
             # For installation, wget (or curl) and cc (build-essential) are also required.
@@ -351,7 +352,7 @@ def get_env_for_install(self):
             "CARGO_HOME": str(directory)
         }
 
-        if platform == "windows":
+        if workstation.is_windows():
             env["RUSTUP_USE_HYPER"] = "1"
 
         return env
diff --git a/multiversx_sdk_cli/downloader.py b/multiversx_sdk_cli/downloader.py
index 908a54c0..5b19d4d3 100644
--- a/multiversx_sdk_cli/downloader.py
+++ b/multiversx_sdk_cli/downloader.py
@@ -35,6 +35,7 @@ def download(url: str, filename: str) -> None:
                 file.write(chunk)
                 progress = _report_download_progress(progress, chunk_number, total_size)
                 chunk_number += 1
+            file.flush()
         print('', file=sys.stderr)
         sys.stderr.flush()
     except requests.HTTPError as err:
diff --git a/multiversx_sdk_cli/errors.py b/multiversx_sdk_cli/errors.py
index 0e8f5369..50b4aa17 100644
--- a/multiversx_sdk_cli/errors.py
+++ b/multiversx_sdk_cli/errors.py
@@ -170,11 +170,6 @@ def __init__(self):
         super().__init__("No wallet provided.")
 
 
-class TestnetError(KnownError):
-    def __init__(self, message: str):
-        super().__init__(message)
-
-
 class LedgerError(KnownError):
     def __init__(self, message: str):
         super().__init__("Ledger error: " + message)
diff --git a/multiversx_sdk_cli/localnet/__init__.py b/multiversx_sdk_cli/localnet/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/multiversx_sdk_cli/localnet/config_default.py b/multiversx_sdk_cli/localnet/config_default.py
new file mode 100644
index 00000000..752362aa
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/config_default.py
@@ -0,0 +1,59 @@
+from pathlib import Path
+
+from multiversx_sdk_cli.localnet.config_general import General
+from multiversx_sdk_cli.localnet.config_networking import Networking
+from multiversx_sdk_cli.localnet.config_sharding import (Metashard,
+                                                         RegularShards)
+from multiversx_sdk_cli.localnet.config_software import (Software,
+                                                         SoftwareChainGo,
+                                                         SoftwareChainProxyGo,
+                                                         SoftwareResolution)
+
+general = General(
+    log_level="*:DEBUG",
+    genesis_delay_seconds=10,
+    rounds_per_epoch=100,
+    round_duration_milliseconds=6000
+)
+
+software = Software(
+    mx_chain_go=SoftwareChainGo(
+        resolution=SoftwareResolution.Remote,
+        archive_url="https://github.com/multiversx/mx-chain-go/archive/refs/heads/master.zip",
+        archive_download_folder=Path("~/multiversx-sdk") / "localnet_software_remote" / "downloaded" / "mx-chain-go",
+        archive_extraction_folder=Path("~/multiversx-sdk") / "localnet_software_remote" / "extracted" / "mx-chain-go",
+        local_path=Path("~/multiversx-sdk") / "localnet_software_local" / "mx-chain-go"
+    ),
+    mx_chain_proxy_go=SoftwareChainProxyGo(
+        resolution=SoftwareResolution.Remote,
+        archive_url="https://github.com/multiversx/mx-chain-proxy-go/archive/refs/heads/master.zip",
+        archive_download_folder=Path("~/multiversx-sdk") / "localnet_software_remote" / "downloaded" / "mx-chain-proxy-go",
+        archive_extraction_folder=Path("~/multiversx-sdk") / "localnet_software_remote" / "extracted" / "mx-chain-proxy-go",
+        local_path=Path("~/multiversx-sdk") / "localnet_software_local" / "mx-chain-proxy-go"
+    )
+)
+
+metashard = Metashard(
+    consensus_size=1,
+    num_observers=0,
+    num_validators=1,
+)
+
+shards = RegularShards(
+    num_shards=2,
+    consensus_size=1,
+    num_observers_per_shard=0,
+    num_validators_per_shard=1,
+)
+
+networking = Networking(
+    host="127.0.0.1",
+    port_seednode=9999,
+    port_seednode_rest_api=10000,
+    p2p_id_seednode="16Uiu2HAkx4QqgXXDdHdUWbLu5kxhd3Uo2hqB2FfCxmxH5Sd7bZFk",
+    port_proxy=7950,
+    port_first_observer=21100,
+    port_first_observer_rest_api=10100,
+    port_first_validator=21500,
+    port_first_validator_rest_api=10200
+)
diff --git a/multiversx_sdk_cli/localnet/config_general.py b/multiversx_sdk_cli/localnet/config_general.py
new file mode 100644
index 00000000..a4579924
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/config_general.py
@@ -0,0 +1,24 @@
+from typing import Any, Dict
+
+from multiversx_sdk_cli.localnet.config_part import ConfigPart
+
+
+class General(ConfigPart):
+    def __init__(self,
+                 log_level: str,
+                 genesis_delay_seconds: int,
+                 rounds_per_epoch: int,
+                 round_duration_milliseconds: int):
+        self.log_level: str = log_level
+        self.genesis_delay_seconds: int = genesis_delay_seconds
+        self.rounds_per_epoch: int = rounds_per_epoch
+        self.round_duration_milliseconds: int = round_duration_milliseconds
+
+    def get_name(self) -> str:
+        return "general"
+
+    def _do_override(self, other: Dict[str, Any]):
+        self.log_level = other.get("log_level", self.log_level)
+        self.genesis_delay_seconds = other.get("genesis_delay_seconds", self.genesis_delay_seconds)
+        self.rounds_per_epoch = other.get("rounds_per_epoch", self.rounds_per_epoch)
+        self.round_duration_milliseconds = other.get("round_duration_milliseconds", self.round_duration_milliseconds)
diff --git a/multiversx_sdk_cli/localnet/config_networking.py b/multiversx_sdk_cli/localnet/config_networking.py
new file mode 100644
index 00000000..f13f877a
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/config_networking.py
@@ -0,0 +1,42 @@
+from typing import Any, Dict
+
+from multiversx_sdk_cli.localnet.config_part import ConfigPart
+
+
+class Networking(ConfigPart):
+    def __init__(self,
+                 host: str,
+                 port_seednode: int,
+                 port_seednode_rest_api: int,
+                 p2p_id_seednode: str,
+                 port_proxy: int,
+                 port_first_observer: int,
+                 port_first_observer_rest_api: int,
+                 port_first_validator: int,
+                 port_first_validator_rest_api: int):
+        self.host: str = host
+        self.port_seednode: int = port_seednode
+        self.port_seednode_rest_api: int = port_seednode_rest_api
+        self.p2p_id_seednode: str = p2p_id_seednode
+        self.port_proxy: int = port_proxy
+        self.port_first_observer: int = port_first_observer
+        self.port_first_observer_rest_api: int = port_first_observer_rest_api
+        self.port_first_validator: int = port_first_validator
+        self.port_first_validator_rest_api: int = port_first_validator_rest_api
+
+    def get_name(self) -> str:
+        return "networking"
+
+    def _do_override(self, other: Dict[str, Any]):
+        self.host = other.get("host", self.host)
+        self.port_seednode = other.get("port_seednode", self.port_seednode)
+        self.port_seednode_rest_api = other.get("port_seednode_rest_api", self.port_seednode_rest_api)
+        self.p2p_id_seednode = other.get("p2p_id_seednode", self.p2p_id_seednode)
+        self.port_proxy = other.get("port_proxy", self.port_proxy)
+        self.port_first_observer = other.get("port_first_observer", self.port_first_observer)
+        self.port_first_observer_rest_api = other.get("port_first_observer_rest_api", self.port_first_observer_rest_api)
+        self.port_first_validator = other.get("port_first_validator", self.port_first_validator)
+        self.port_first_validator_rest_api = other.get("port_first_validator_rest_api", self.port_first_validator_rest_api)
+
+    def get_proxy_url(self) -> str:
+        return f"http://{self.host}:{self.port_proxy}"
diff --git a/multiversx_sdk_cli/localnet/config_part.py b/multiversx_sdk_cli/localnet/config_part.py
new file mode 100644
index 00000000..e2df004f
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/config_part.py
@@ -0,0 +1,36 @@
+import logging
+from typing import Any, Dict
+
+from multiversx_sdk_cli.errors import UnknownConfigurationError
+
+logger = logging.getLogger("localnet")
+
+
+class ConfigPart:
+    def __init__(self):
+        pass
+
+    def get_name(self) -> str:
+        raise NotImplementedError()
+
+    def override(self, other: Dict[str, Any]):
+        self._validate_overriding_entries(other)
+        self._do_override(other)
+
+    def _validate_overriding_entries(self, overriding: Dict[str, Any]) -> None:
+        allowed_entries = set(self.__dict__.keys())
+        overriding_entries = set(overriding.keys())
+        unknown_entries = overriding_entries - allowed_entries
+
+        if unknown_entries:
+            logger.error(f"""\
+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}")
+
+    def _do_override(self, other: Dict[str, Any]) -> None:
+        raise NotImplementedError()
+
+    def to_dictionary(self) -> Dict[str, Any]:
+        return self.__dict__
diff --git a/multiversx_sdk_cli/localnet/config_root.py b/multiversx_sdk_cli/localnet/config_root.py
new file mode 100644
index 00000000..575a44ad
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/config_root.py
@@ -0,0 +1,172 @@
+import logging
+import time
+from pathlib import Path
+from typing import Any, Dict, List
+
+import toml
+
+from multiversx_sdk_cli.localnet import config_default
+from multiversx_sdk_cli.localnet.config_part import ConfigPart
+from multiversx_sdk_cli.localnet.constants import METACHAIN_ID
+from multiversx_sdk_cli.localnet.node import Node
+
+logger = logging.getLogger("localnet")
+
+
+class ConfigRoot(ConfigPart):
+    def __init__(self):
+        self.general = config_default.general
+        self.software = config_default.software
+        self.metashard = config_default.metashard
+        self.shards = config_default.shards
+        self.networking = config_default.networking
+
+    def get_name(self) -> str:
+        return "(configuration root)"
+
+    def _do_override(self, other: Dict[str, Any]):
+        self.general.override(other.get(self.general.get_name(), dict()))
+        self.software.override(other.get(self.software.get_name(), dict()))
+        self.metashard.override(other.get(self.metashard.get_name(), dict()))
+        self.shards.override(other.get(self.shards.get_name(), dict()))
+        self.networking.override(other.get(self.networking.get_name(), dict()))
+
+    @classmethod
+    def from_file(cls, path: Path):
+        logger.info(f"Loading localnet configuration from: {path}")
+
+        path = path.expanduser().resolve()
+        instance = cls()
+        local_config_dict = toml.load(str(path))
+        instance.override(local_config_dict)
+
+        return instance
+
+    def save(self, path: Path):
+        path = path.expanduser().resolve()
+        with open(path, "w") as f:
+            toml.dump(self.to_dictionary(), f)
+
+        logger.info(f"Saved localnet configuration to: {path}")
+
+    def root(self) -> Path:
+        return Path("localnet").expanduser().resolve()
+
+    def seednode_folder(self):
+        return self.root() / "seednode"
+
+    def seednode_config_folder(self):
+        return self.seednode_folder() / "config"
+
+    def proxy_folder(self):
+        return self.root() / "proxy"
+
+    def proxy_config_folder(self):
+        return self.proxy_folder() / "config"
+
+    def all_nodes_folders(self):
+        return self.validator_folders() + self.observer_folders()
+
+    def all_nodes_config_folders(self):
+        return self.validator_config_folders() + self.observer_config_folders()
+
+    def genesis_time(self):
+        return int(time.time()) + int(self.general.genesis_delay_seconds)
+
+    def seednode_address(self):
+        host = self.networking.host
+        port = self.networking.port_seednode
+        identifier = self.networking.p2p_id_seednode
+        return f"/ip4/{host}/tcp/{port}/p2p/{identifier}"
+
+    def seednode_api_interface(self):
+        port = self.networking.port_seednode_rest_api
+        return f"{self.networking.host}:{port}"
+
+    def seednode_api_address(self):
+        return f"http://{self.seednode_api_interface()}"
+
+    def num_all_nodes(self) -> int:
+        return self.num_all_validators() + self.num_all_observers()
+
+    def num_all_validators(self) -> int:
+        return self.shards.num_shards * self.shards.num_validators_per_shard + self.metashard.num_validators
+
+    def num_all_observers(self) -> int:
+        return self.shards.num_shards * self.shards.num_observers_per_shard + self.metashard.num_observers
+
+    def all_nodes(self) -> List[Node]:
+        return self.validators() + self.observers()
+
+    def validators(self) -> List[Node]:
+        first_port = self.networking.port_first_validator_rest_api
+        nodes: List[Node] = []
+
+        for i, folder in enumerate(self.validator_folders()):
+            shard = self._get_shard_of_validator(i)
+            host = self.networking.host
+            port = first_port + i
+            nodes.append(Node(index=i, folder=folder, shard=str(shard), host=host, api_port=port))
+
+        return nodes
+
+    def _get_shard_of_validator(self, observer_index: int) -> int:
+        shard = int(observer_index // self.shards.num_validators_per_shard)
+        return shard if shard < self.shards.num_shards else METACHAIN_ID
+
+    def validator_folders(self):
+        return [self.root() / "validator{:02}".format(i) for i in range(self.num_all_validators())]
+
+    def validator_config_folders(self) -> List[Path]:
+        return [folder / "config" for folder in self.validator_folders()]
+
+    def observers(self) -> List[Node]:
+        first_port = self.networking.port_first_observer_rest_api
+        nodes: List[Node] = []
+
+        for i, folder in enumerate(self.observer_folders()):
+            shard = self._get_shard_of_observer(i)
+            host = self.networking.host
+            port = first_port + i
+            nodes.append(Node(index=i, folder=folder, shard=str(shard), host=host, api_port=port))
+
+        return nodes
+
+    def _get_shard_of_observer(self, observer_index: int):
+        shard = int(observer_index // self.shards.num_observers_per_shard)
+        return shard if shard < self.shards.num_shards else METACHAIN_ID
+
+    def observer_config_folders(self) -> List[Path]:
+        return [folder / "config" for folder in self.observer_folders()]
+
+    def observer_folders(self) -> List[Path]:
+        return [self.root() / "observer{:02}".format(i) for i in range(self.num_all_observers())]
+
+    def api_addresses_sharded_for_proxy_config(self) -> List[Dict[str, Any]]:
+        nodes: List[Dict[str, Any]] = []
+
+        for node in self.observers():
+            nodes.append({
+                "ShardId": int(node.shard),
+                "Address": node.api_address(),
+                "Type": "Observer"
+            })
+
+        for node in self.validators():
+            nodes.append({
+                "ShardId": int(node.shard),
+                "Address": node.api_address(),
+                "Type": "Validator"
+            })
+
+        return nodes
+
+    def to_dictionary(self) -> Dict[str, Any]:
+        result: Dict[str, Any] = dict()
+        result[self.general.get_name()] = self.general.to_dictionary()
+        result[self.software.get_name()] = self.software.to_dictionary()
+        result[self.metashard.get_name()] = self.metashard.to_dictionary()
+        result[self.shards.get_name()] = self.shards.to_dictionary()
+        result[self.networking.get_name()] = self.networking.to_dictionary()
+
+        return result
diff --git a/multiversx_sdk_cli/localnet/config_sharding.py b/multiversx_sdk_cli/localnet/config_sharding.py
new file mode 100644
index 00000000..d509a1db
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/config_sharding.py
@@ -0,0 +1,42 @@
+from typing import Any, Dict
+
+from multiversx_sdk_cli.localnet.config_part import ConfigPart
+
+
+class Metashard(ConfigPart):
+    def __init__(self,
+                 consensus_size: int,
+                 num_observers: int,
+                 num_validators: int):
+        self.consensus_size: int = consensus_size
+        self.num_observers: int = num_observers
+        self.num_validators: int = num_validators
+
+    def get_name(self) -> str:
+        return "metashard"
+
+    def _do_override(self, other: Dict[str, Any]):
+        self.consensus_size = other.get("consensus_size", self.consensus_size)
+        self.num_observers = other.get("num_observers", self.num_observers)
+        self.num_validators = other.get("num_validators", self.num_validators)
+
+
+class RegularShards(ConfigPart):
+    def __init__(self,
+                 num_shards: int,
+                 consensus_size: int,
+                 num_observers_per_shard: int,
+                 num_validators_per_shard: int):
+        self.num_shards: int = num_shards
+        self.consensus_size: int = consensus_size
+        self.num_observers_per_shard: int = num_observers_per_shard
+        self.num_validators_per_shard: int = num_validators_per_shard
+
+    def get_name(self) -> str:
+        return "shards"
+
+    def _do_override(self, other: Dict[str, Any]):
+        self.num_shards = other.get("num_shards", self.num_shards)
+        self.consensus_size = other.get("consensus_size", self.consensus_size)
+        self.num_observers_per_shard = other.get("num_observers_per_shard", self.num_observers_per_shard)
+        self.num_validators_per_shard = other.get("num_validators_per_shard", self.num_validators_per_shard)
diff --git a/multiversx_sdk_cli/localnet/config_software.py b/multiversx_sdk_cli/localnet/config_software.py
new file mode 100644
index 00000000..ca8cdf2b
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/config_software.py
@@ -0,0 +1,176 @@
+from enum import Enum
+from pathlib import Path
+from typing import Any, Dict
+
+from multiversx_sdk_cli.errors import KnownError
+from multiversx_sdk_cli.localnet.config_part import ConfigPart
+
+
+class SoftwareResolution(Enum):
+    Remote = "remote"
+    Local = "local"
+
+
+class Software(ConfigPart):
+    def __init__(
+            self,
+            mx_chain_go: 'SoftwareChainGo',
+            mx_chain_proxy_go: 'SoftwareChainProxyGo'):
+        self.mx_chain_go = mx_chain_go
+        self.mx_chain_proxy_go = mx_chain_proxy_go
+
+    def get_name(self) -> str:
+        return "software"
+
+    def _do_override(self, other: Dict[str, Any]):
+        self.mx_chain_go.override(other.get("mx_chain_go", {}))
+        self.mx_chain_proxy_go.override(other.get("mx_chain_proxy_go", {}))
+
+    def to_dictionary(self) -> Dict[str, Any]:
+        return {
+            "mx_chain_go": self.mx_chain_go.to_dictionary(),
+            "mx_chain_proxy_go": self.mx_chain_proxy_go.to_dictionary(),
+        }
+
+
+class SoftwareComponent(ConfigPart):
+    def __init__(self,
+                 resolution: SoftwareResolution,
+                 archive_url: str,
+                 archive_download_folder: Path,
+                 archive_extraction_folder: Path,
+                 local_path: Path):
+        self.resolution: SoftwareResolution = resolution
+        self.archive_url: str = archive_url
+        self.archive_download_folder: Path = archive_download_folder
+        self.archive_extraction_folder: Path = archive_extraction_folder
+        self.local_path: Path = local_path
+        self._verify()
+
+    def _do_override(self, other: Dict[str, Any]) -> None:
+        self.resolution = SoftwareResolution(other.get("resolution", self.resolution))
+        self.archive_url = other.get("archive_url", self.archive_url)
+        self.archive_download_folder = Path(other.get("archive_download_folder", self.archive_download_folder))
+        self.archive_extraction_folder = Path(other.get("archive_extraction_folder", self.archive_extraction_folder))
+        self.local_path = Path(other.get("local_path", self.local_path))
+        self._verify()
+
+    def _verify(self):
+        if self.resolution == SoftwareResolution.Remote:
+            if not self.archive_url:
+                raise KnownError(f"In configuration section '{self.get_name()}', resolution is '{self.resolution.value}', but 'archive_url' is bad (empty)")
+        if self.resolution == SoftwareResolution.Local:
+            if not self.get_local_path().is_dir():
+                raise KnownError(f"In configuration section '{self.get_name()}', resolution is '{self.resolution.value}', but 'local_path' is not a directory: {self.local_path}")
+
+    def get_archive_download_folder(self):
+        return self.archive_download_folder.expanduser().resolve()
+
+    def get_archive_extraction_folder(self):
+        return self.archive_extraction_folder.expanduser().resolve()
+
+    def get_local_path(self):
+        return self.local_path.expanduser().resolve()
+
+    def get_path_within_source(self, relative_path: Path) -> Path:
+        path = self._get_source_folder() / relative_path
+        return path.expanduser().resolve()
+
+    def _get_source_folder(self) -> Path:
+        if self.resolution == SoftwareResolution.Remote:
+            return self._locate_source_folder_in_archive_extraction_folder()
+        if self.resolution == SoftwareResolution.Local:
+            return self.local_path
+
+        raise KnownError(f"Unknown resolution: {self.resolution}")
+
+    def _locate_source_folder_in_archive_extraction_folder(self) -> Path:
+        extraction_folder = self.get_archive_extraction_folder()
+
+        # If has one subfolder, that one is the source code
+        subfolders = list(extraction_folder.glob("*"))
+        source_folder = subfolders[0] if len(subfolders) == 1 else extraction_folder
+        # Heuristic to check if this is a valid source code folder
+        assert (source_folder / "go.mod").exists(), f"This is not a valid source code folder: {source_folder}"
+        return source_folder
+
+    def to_dictionary(self) -> Dict[str, Any]:
+        return {
+            "resolution": self.resolution.value,
+            "archive_url": self.archive_url,
+            "archive_download_folder": str(self.archive_download_folder),
+            "archive_extraction_folder": str(self.archive_extraction_folder),
+            "local_path": str(self.local_path) if self.local_path else None,
+        }
+
+
+class SoftwareChainGo(SoftwareComponent):
+    def get_name(self) -> str:
+        return "mx_chain_go"
+
+    def get_cmd_node_folder(self):
+        folder = self._get_cmd_folder() / "node"
+        folder_must_exist(folder)
+        return folder
+
+    def get_cmd_seednode_folder(self):
+        folder = self._get_cmd_folder() / "seednode"
+        folder_must_exist(folder)
+        return folder
+
+    def _get_cmd_folder(self):
+        folder = self.get_path_within_source(Path("cmd"))
+        folder_must_exist(folder)
+        return folder
+
+    def is_node_built(self):
+        return (self.get_cmd_node_folder() / "node").exists()
+
+    def is_seednode_built(self):
+        return (self.get_cmd_seednode_folder() / "seednode").exists()
+
+    def get_node_config_folder(self):
+        return self.get_cmd_node_folder() / "config"
+
+    def get_seednode_config_folder(self):
+        return self.get_cmd_seednode_folder() / "config"
+
+    def node_config_must_exist(self):
+        folder_must_exist(self.get_node_config_folder())
+
+    def seednode_config_must_exist(self):
+        folder_must_exist(self.get_seednode_config_folder())
+
+
+class SoftwareChainProxyGo(SoftwareComponent):
+    def get_name(self) -> str:
+        return "mx_chain_proxy_go"
+
+    def get_cmd_proxy_folder(self):
+        folder = self._get_cmd_folder() / "proxy"
+        folder_must_exist(folder)
+        return folder
+
+    def _get_cmd_folder(self):
+        folder = self.get_path_within_source(Path("cmd"))
+        folder_must_exist(folder)
+        return folder
+
+    def is_proxy_built(self):
+        return (self.get_cmd_proxy_folder() / "proxy").exists()
+
+    def get_proxy_config_folder(self):
+        return self.get_cmd_proxy_folder() / "config"
+
+    def proxy_config_must_exist(self):
+        folder_must_exist(self.get_proxy_config_folder())
+
+
+def folder_must_exist(path: Path) -> None:
+    if not path.exists():
+        raise KnownError(f"Folder does not exist: {path}")
+
+
+def file_must_exist(path: Path) -> None:
+    if not path.exists():
+        raise KnownError(f"File does not exist: {path}")
diff --git a/multiversx_sdk_cli/localnet/constants.py b/multiversx_sdk_cli/localnet/constants.py
new file mode 100644
index 00000000..26ef194c
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/constants.py
@@ -0,0 +1,6 @@
+import stat
+
+METACHAIN_ID = 4294967295
+NETWORK_MONITORING_INTERVAL_IN_SECONDS = 1
+# Read, write and execute by owner, read and execute by group and others
+FILE_MODE_EXECUTABLE = stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH
diff --git a/multiversx_sdk_cli/testnet/genesis.py b/multiversx_sdk_cli/localnet/genesis.py
similarity index 92%
rename from multiversx_sdk_cli/testnet/genesis.py
rename to multiversx_sdk_cli/localnet/genesis.py
index bf81dea9..453adcaa 100644
--- a/multiversx_sdk_cli/testnet/genesis.py
+++ b/multiversx_sdk_cli/localnet/genesis.py
@@ -1,6 +1,6 @@
 from multiversx_sdk_cli.accounts import Address
 from multiversx_sdk_cli.contracts import SmartContract
-from multiversx_sdk_cli.testnet import wallets
+from multiversx_sdk_cli.localnet import wallets
 
 
 def get_owner_of_genesis_contracts():
diff --git a/multiversx_sdk_cli/testnet/genesis_json.py b/multiversx_sdk_cli/localnet/genesis_json.py
similarity index 74%
rename from multiversx_sdk_cli/testnet/genesis_json.py
rename to multiversx_sdk_cli/localnet/genesis_json.py
index 38bb6262..f7348ffb 100644
--- a/multiversx_sdk_cli/testnet/genesis_json.py
+++ b/multiversx_sdk_cli/localnet/genesis_json.py
@@ -1,19 +1,20 @@
-from typing import Any, List
+from typing import Any, Dict, List
 
 from multiversx_sdk_cli.accounts import Account, Address
-from multiversx_sdk_cli.testnet import wallets
-from multiversx_sdk_cli.testnet.config import TestnetConfiguration
-from multiversx_sdk_cli.testnet.genesis import (get_delegation_address,
-                                   is_foundational_node, is_last_user)
+from multiversx_sdk_cli.localnet import wallets
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+from multiversx_sdk_cli.localnet.genesis import (get_delegation_address,
+                                                 is_foundational_node,
+                                                 is_last_user)
 
 ENTIRE_SUPPLY = 20000000000000000000000000
-# For testnet, we delegate for 1 node
+# For localnet, we delegate for 1 node
 DELEGATED_VALUE = 2500000000000000000000
 
 
-def build(testnet_config: TestnetConfiguration) -> List[Any]:
-    num_validators = testnet_config.num_all_validators()
-    genesis_items = []
+def build(config: ConfigRoot) -> List[Any]:
+    num_validators = config.num_all_validators()
+    genesis_items: List[Dict[str, Any]] = []
     remaining_supply = ENTIRE_SUPPLY
     remaining_to_delegate = DELEGATED_VALUE
     delegation_address = get_delegation_address()
@@ -39,7 +40,7 @@ def build(testnet_config: TestnetConfiguration) -> List[Any]:
     return genesis_items
 
 
-def _build_validator_entry(nickname: str, account: Account, value: int):
+def _build_validator_entry(nickname: str, account: Account, value: int) -> Dict[str, Any]:
     return {
         "nickname": nickname,
         "address": account.address.bech32(),
@@ -53,7 +54,7 @@ def _build_validator_entry(nickname: str, account: Account, value: int):
     }
 
 
-def _build_user_entry(nickname: str, account: Account, value: int, delegated_value: int, delegation_address: Address):
+def _build_user_entry(nickname: str, account: Account, value: int, delegated_value: int, delegation_address: Address) -> Dict[str, Any]:
     return {
         "nickname": nickname,
         "address": account.address.bech32(),
diff --git a/multiversx_sdk_cli/testnet/genesis_smart_contracts_json.py b/multiversx_sdk_cli/localnet/genesis_smart_contracts_json.py
similarity index 56%
rename from multiversx_sdk_cli/testnet/genesis_smart_contracts_json.py
rename to multiversx_sdk_cli/localnet/genesis_smart_contracts_json.py
index a0bc23cf..d50a7601 100644
--- a/multiversx_sdk_cli/testnet/genesis_smart_contracts_json.py
+++ b/multiversx_sdk_cli/localnet/genesis_smart_contracts_json.py
@@ -1,10 +1,10 @@
 from typing import Any
 
-from multiversx_sdk_cli.testnet import genesis
-from multiversx_sdk_cli.testnet.config import TestnetConfiguration
+from multiversx_sdk_cli.localnet import genesis
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
 
 
-def patch(data: Any, testnet_config: TestnetConfiguration):
+def patch(data: Any, config: ConfigRoot):
     owner = genesis.get_owner_of_genesis_contracts()
 
     delegation_config = data[0]
@@ -12,5 +12,5 @@ def patch(data: Any, testnet_config: TestnetConfiguration):
 
     delegation_config["owner"] = owner.address.bech32()
     dns_config["owner"] = owner.address.bech32()
-    # registration price = 100 atoms of eGLD
+    # registration price = 100 atto-EGLD
     dns_config["init-parameters"] = "0064"
diff --git a/multiversx_sdk_cli/localnet/libraries.py b/multiversx_sdk_cli/localnet/libraries.py
new file mode 100644
index 00000000..731981d8
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/libraries.py
@@ -0,0 +1,18 @@
+import logging
+import os
+import shutil
+from pathlib import Path
+from typing import List
+
+from multiversx_sdk_cli.localnet.constants import FILE_MODE_EXECUTABLE
+
+logger = logging.getLogger("localnet")
+
+
+def copy_libraries(source: Path, destination: Path):
+    libraries: List[Path] = list(source.glob("*.dylib")) + list(source.glob("*.so"))
+
+    for library in libraries:
+        logger.debug(f"Copying {library} to {destination}")
+        shutil.copy(library, destination)
+        os.chmod(destination / library.name, FILE_MODE_EXECUTABLE)
diff --git a/multiversx_sdk_cli/localnet/node.py b/multiversx_sdk_cli/localnet/node.py
new file mode 100644
index 00000000..7e02f85b
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/node.py
@@ -0,0 +1,27 @@
+from pathlib import Path
+
+
+class Node:
+    def __init__(self, index: int, folder: Path, shard: str, host: str, api_port: int) -> None:
+        self.index = index
+        self.folder = folder
+        self.shard = shard
+        self.host = host
+        self.api_port = api_port
+
+    def key_file_path(self):
+        return self.folder / "config" / "validatorKey.pem"
+
+    def api_address(self):
+        return f"http://{self.api_interface()}"
+
+    def api_interface(self):
+        return f"{self.host}:{self.api_port}"
+
+    def __repr__(self) -> str:
+        return f"Node {self.index}, shard={self.shard}, port={self.api_port}, folder={self.folder}"
+
+
+class NodeStatus:
+    def __init__(self, nonce: int) -> None:
+        self.nonce = nonce
diff --git a/multiversx_sdk_cli/localnet/node_config_toml.py b/multiversx_sdk_cli/localnet/node_config_toml.py
new file mode 100644
index 00000000..f0107806
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/node_config_toml.py
@@ -0,0 +1,71 @@
+from typing import Any, Dict
+
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+from multiversx_sdk_cli.localnet.nodes_setup_json import CHAIN_ID
+
+ConfigDict = Dict[str, Any]
+
+
+def patch_config(data: ConfigDict, config: ConfigRoot):
+    data['DbLookupExtensions']['Enabled'] = True
+
+    general_settings: ConfigDict = dict()
+    general_settings['ChainID'] = CHAIN_ID
+    general_settings['StartInEpochEnabled'] = False
+    general_settings['SetGuardianEpochsDelay'] = 1
+
+    data['GeneralSettings'].update(general_settings)
+
+    # Make epochs shorter
+    epoch_start_config: ConfigDict = dict()
+    epoch_start_config['RoundsPerEpoch'] = config.general.rounds_per_epoch
+    epoch_start_config['MinRoundsBetweenEpochs'] = int(config.general.rounds_per_epoch / 4)
+
+    data['EpochStartConfig'].update(epoch_start_config)
+
+    # Always use the latest VM
+    virtual_machine: Dict[str, Any] = dict()
+    virtual_machine['Execution'] = dict()
+    virtual_machine['Execution']['WasmVMVersions'] = [{'StartEpoch': 0, 'Version': '*'}]
+    virtual_machine['Querying'] = dict()
+    virtual_machine['Querying']['NumConcurrentVMs'] = 1
+    virtual_machine['Querying']['WasmVMVersions'] = [{'StartEpoch': 0, 'Version': '*'}]
+
+    data['VirtualMachine'].update(virtual_machine)
+
+
+def patch_api(data: ConfigDict, config: ConfigRoot):
+    routes = data['APIPackages']['transaction']['Routes']
+    for route in routes:
+        route['Open'] = True
+
+
+def patch_enable_epochs(data: ConfigDict, config: ConfigRoot):
+    enable_epochs: ConfigDict = dict()
+    enable_epochs['SCDeployEnableEpoch'] = 0
+    enable_epochs['BuiltInFunctionsEnableEpoch'] = 0
+    enable_epochs['RelayedTransactionsEnableEpoch'] = 0
+    enable_epochs['PenalizedTooMuchGasEnableEpoch'] = 0
+    enable_epochs['SwitchJailWaitingEnableEpoch'] = 0
+    enable_epochs['BelowSignedThresholdEnableEpoch'] = 0
+    enable_epochs['AheadOfTimeGasUsageEnableEpoch'] = 0
+    enable_epochs['GasPriceModifierEnableEpoch'] = 0
+    enable_epochs['RepairCallbackEnableEpoch'] = 0
+    enable_epochs['BlockGasAndFeesReCheckEnableEpoch'] = 0
+    enable_epochs['ReturnDataToLastTransferEnableEpoch'] = 0
+    enable_epochs['SenderInOutTransferEnableEpoch'] = 0
+    enable_epochs['ESDTEnableEpoch'] = 0
+    enable_epochs['IncrementSCRNonceInMultiTransferEnableEpoch'] = 0
+    enable_epochs['ESDTMultiTransferEnableEpoch'] = 0
+    enable_epochs['GlobalMintBurnDisableEpoch'] = 0
+    enable_epochs['ESDTTransferRoleEnableEpoch'] = 0
+    enable_epochs['BuiltInFunctionOnMetaEnableEpoch'] = 0
+    enable_epochs['MultiESDTTransferFixOnCallBackOnEnableEpoch'] = 0
+    enable_epochs['ESDTNFTCreateOnMultiShard'] = 0
+    enable_epochs['RemoveNonUpdatedStorageEnableEpoch'] = 0
+    enable_epochs['FixOOGReturnCodeEnableEpoch'] = 0
+    enable_epochs['CorrectFirstQueuedEpoch'] = 0
+    enable_epochs['MetaESDTSetEnableEpoch'] = 0
+    enable_epochs['DelegationManagerEnableEpoch'] = 0
+
+    data['EnableEpochs'].update(enable_epochs)
diff --git a/multiversx_sdk_cli/testnet/nodes_setup_json.py b/multiversx_sdk_cli/localnet/nodes_setup_json.py
similarity index 51%
rename from multiversx_sdk_cli/testnet/nodes_setup_json.py
rename to multiversx_sdk_cli/localnet/nodes_setup_json.py
index 5efe7ac3..00d47291 100644
--- a/multiversx_sdk_cli/testnet/nodes_setup_json.py
+++ b/multiversx_sdk_cli/localnet/nodes_setup_json.py
@@ -1,33 +1,34 @@
-from typing import Any
+from typing import Any, Dict, List
 
 from multiversx_sdk_cli.accounts import Account
-from multiversx_sdk_cli.testnet import wallets
-from multiversx_sdk_cli.testnet.config import TestnetConfiguration
-from multiversx_sdk_cli.testnet.genesis import get_delegation_address, is_foundational_node
+from multiversx_sdk_cli.localnet import wallets
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+from multiversx_sdk_cli.localnet.genesis import (get_delegation_address,
+                                                 is_foundational_node)
 
-CHAIN_ID = "local-testnet"
+CHAIN_ID = "localnet"
 
 
-def build(testnet_config: TestnetConfiguration) -> Any:
-    num_validators = testnet_config.num_all_validators()
-    initial_nodes = []
+def build(config: ConfigRoot) -> Any:
+    num_validators = config.num_all_validators()
+    initial_nodes: List[Dict[str, str]] = []
 
     for nickname, [pubkey, account] in wallets.get_validators(num_validators).items():
         entry = _build_initial_nodes_entry(nickname, pubkey, account)
         initial_nodes.append(entry)
 
     # Then, patch the list of initial nodes, so that higher indexes will become metachain nodes.
-    num_metachain_nodes = testnet_config.num_validators_in_metashard()
+    num_metachain_nodes = config.metashard.num_validators
     num_nodes = len(initial_nodes)
     initial_nodes = initial_nodes[num_nodes - num_metachain_nodes:] + initial_nodes[:num_nodes - num_metachain_nodes]
 
     return {
-        "startTime": testnet_config.genesis_time(),
-        "roundDuration": 6000,
-        "consensusGroupSize": testnet_config.shards["consensus_size"],
-        "minNodesPerShard": testnet_config.shards["consensus_size"],
-        "metaChainConsensusGroupSize": testnet_config.metashard["consensus_size"],
-        "metaChainMinNodes": testnet_config.metashard["validators"],
+        "startTime": config.genesis_time(),
+        "roundDuration": config.general.round_duration_milliseconds,
+        "consensusGroupSize": config.shards.consensus_size,
+        "minNodesPerShard": config.shards.consensus_size,
+        "metaChainConsensusGroupSize": config.metashard.consensus_size,
+        "metaChainMinNodes": config.metashard.num_validators,
         "hysteresis": 0,
         "adaptivity": False,
         "chainID": CHAIN_ID,
@@ -36,7 +37,7 @@ def build(testnet_config: TestnetConfiguration) -> Any:
     }
 
 
-def _build_initial_nodes_entry(nickname: str, pubkey: str, account: Account) -> Any:
+def _build_initial_nodes_entry(nickname: str, pubkey: str, account: Account) -> Dict[str, str]:
     address = get_delegation_address().bech32() if is_foundational_node(nickname) else account.address.bech32()
 
     return {
diff --git a/multiversx_sdk_cli/testnet/p2p_toml.py b/multiversx_sdk_cli/localnet/p2p_toml.py
similarity index 61%
rename from multiversx_sdk_cli/testnet/p2p_toml.py
rename to multiversx_sdk_cli/localnet/p2p_toml.py
index 77526a9d..b4e5a4f4 100644
--- a/multiversx_sdk_cli/testnet/p2p_toml.py
+++ b/multiversx_sdk_cli/localnet/p2p_toml.py
@@ -1,22 +1,22 @@
 from typing import Any
 
-from multiversx_sdk_cli.testnet.config import TestnetConfiguration
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
 
 PROTOCOL_ID = '/erd/kad/sandbox'
 
 
-def patch(data: Any, testnet_config: TestnetConfiguration, node_index: int, port_first: int) -> Any:
+def patch(data: Any, config: ConfigRoot, node_index: int, port_first: int) -> Any:
     data['Node']['Port'] = str(port_first + node_index)
     data['Node']['ThresholdMinConnectedPeers'] = 1
     data['KadDhtPeerDiscovery']['InitialPeerList'] = [
-        testnet_config.seednode_address()
+        config.seednode_address()
     ]
     data['KadDhtPeerDiscovery']['ProtocolID'] = PROTOCOL_ID
     data['Sharding']['Type'] = "NilListSharder"
 
 
-def patch_for_seednode(data: Any, testnet_config: TestnetConfiguration):
-    port_seednode = testnet_config.networking['port_seednode']
+def patch_for_seednode(data: Any, config: ConfigRoot):
+    port_seednode = config.networking.port_seednode
 
     data['Node']['Port'] = str(port_seednode)
     data['Node']['MaximumExpectedPeerCount'] = 16
diff --git a/multiversx_sdk_cli/testnet/seednode_p2pKey.pem b/multiversx_sdk_cli/localnet/seednode_p2pKey.pem
similarity index 100%
rename from multiversx_sdk_cli/testnet/seednode_p2pKey.pem
rename to multiversx_sdk_cli/localnet/seednode_p2pKey.pem
diff --git a/multiversx_sdk_cli/localnet/step_build_software.py b/multiversx_sdk_cli/localnet/step_build_software.py
new file mode 100644
index 00000000..34c46e79
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/step_build_software.py
@@ -0,0 +1,56 @@
+import logging
+import subprocess
+from pathlib import Path
+from typing import Dict, List
+
+from multiversx_sdk_cli import dependencies, utils
+from multiversx_sdk_cli.errors import KnownError
+from multiversx_sdk_cli.localnet import libraries
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+
+logger = logging.getLogger("localnet")
+
+
+def build(configfile: Path, software_components: List[str]):
+    config = ConfigRoot.from_file(configfile)
+
+    golang = dependencies.get_golang()
+    golang_env = golang.get_env()
+
+    if "node" in software_components:
+        logger.info("Building node...")
+
+        cmd_node = config.software.mx_chain_go.get_cmd_node_folder()
+        _do_build(cmd_node, golang_env)
+
+        wasmer_path = golang.get_gopath() / "pkg" / "mod" / _get_wasm_vm_package(config) / "wasmer"
+        libraries.copy_libraries(wasmer_path, cmd_node)
+
+    if "seednode" in software_components:
+        logger.info("Building seednode...")
+
+        cmd_seednode = config.software.mx_chain_go.get_cmd_seednode_folder()
+        _do_build(cmd_seednode, golang_env)
+
+        wasmer_path = golang.get_gopath() / "pkg" / "mod" / _get_wasm_vm_package(config) / "wasmer"
+        libraries.copy_libraries(wasmer_path, cmd_seednode)
+
+    if "proxy" in software_components:
+        logger.info("Building proxy...")
+
+        cmd_proxy = config.software.mx_chain_proxy_go.get_cmd_proxy_folder()
+        _do_build(cmd_proxy, golang_env)
+
+
+def _do_build(cwd: Path, env: Dict[str, str]):
+    return_code = subprocess.check_call(["go", "build"], cwd=cwd, env=env)
+    if return_code != 0:
+        raise KnownError(f"error code = {return_code}, see output")
+
+
+def _get_wasm_vm_package(config: ConfigRoot) -> str:
+    go_mod = config.software.mx_chain_go.get_path_within_source(Path("go.mod"))
+    lines = utils.read_lines(go_mod)
+    line = [line for line in lines if "github.com/multiversx/mx-chain-vm-v" in line][-1]
+    parts = line.split()
+    return f"{parts[0]}@{parts[1]}"
diff --git a/multiversx_sdk_cli/localnet/step_clean.py b/multiversx_sdk_cli/localnet/step_clean.py
new file mode 100644
index 00000000..c3507609
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/step_clean.py
@@ -0,0 +1,14 @@
+import logging
+from pathlib import Path
+
+from multiversx_sdk_cli import utils
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+
+logger = logging.getLogger("localnet")
+
+
+def clean(configfile: Path):
+    logger.info("clean()")
+
+    config = ConfigRoot.from_file(configfile)
+    utils.remove_folder(config.root())
diff --git a/multiversx_sdk_cli/localnet/step_config.py b/multiversx_sdk_cli/localnet/step_config.py
new file mode 100644
index 00000000..ba3a4447
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/step_config.py
@@ -0,0 +1,220 @@
+import logging
+import shutil
+from os import path
+from pathlib import Path
+from typing import List
+
+import multiversx_sdk_cli.utils as utils
+from multiversx_sdk_cli.errors import KnownError
+from multiversx_sdk_cli.localnet import (genesis_json,
+                                         genesis_smart_contracts_json,
+                                         libraries, node_config_toml,
+                                         nodes_setup_json, p2p_toml, wallets)
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+
+logger = logging.getLogger("localnet")
+
+
+def configure(configfile: Path):
+    logger.info("configure()")
+
+    config = ConfigRoot.from_file(configfile)
+
+    if config.root().exists():
+        error_message = f"Localnet folder already exists: {config.root()}. Perhaps run 'mxpy localnet clean' first?"
+        logger.error(error_message)
+        raise KnownError(error_message)
+
+    logger.info("Localnet folder is %s", config.root())
+
+    create_folders(config)
+
+    # Validators and Observers
+    copy_config_to_nodes(config)
+    copy_validator_keys(config)
+    patch_node_config(config)
+
+    patch_nodes_p2p_config(
+        config,
+        config.validator_config_folders(),
+        config.networking.port_first_validator,
+    )
+    patch_nodes_p2p_config(
+        config,
+        config.observer_config_folders(),
+        config.networking.port_first_observer,
+    )
+    overwrite_nodes_setup(
+        config,
+        config.validator_config_folders(),
+    )
+    overwrite_nodes_setup(
+        config,
+        config.observer_config_folders(),
+    )
+    overwrite_genesis_file(
+        config,
+        config.validator_config_folders()
+    )
+    overwrite_genesis_file(
+        config,
+        config.observer_config_folders()
+    )
+
+    # Seed node
+    copy_config_to_seednode(config)
+    patch_seednode_p2p_config(config)
+    copy_seednode_p2p_key(config)
+
+    # Proxy
+    copy_config_to_proxy(config)
+    patch_proxy_config(config)
+
+    copy_binaries_into_localnet_workspace(config)
+
+
+def create_folders(config: ConfigRoot):
+    makefolder(config.seednode_folder())
+
+    folder = config.proxy_folder()
+    makefolder(folder)
+    makefolder(folder / 'config')
+
+    for folder in config.all_nodes_folders():
+        makefolder(folder)
+
+
+def copy_config_to_nodes(config: ConfigRoot):
+    config_prototype = config.software.mx_chain_go.get_node_config_folder()
+    for node_config in config.all_nodes_config_folders():
+        shutil.copytree(config_prototype, node_config)
+
+
+def copy_validator_keys(config: ConfigRoot):
+    for index, validator in enumerate(config.validators()):
+        shutil.copy(wallets.get_validator_key_file(index), validator.key_file_path())
+
+    # Currently, observers require validator PEM files as well (we have to adjust this and use --no-key parameter)
+    for index, observer in enumerate(config.observers()):
+        shutil.copy(wallets.get_observer_key_file(index), observer.key_file_path())
+
+
+def patch_node_config(config: ConfigRoot):
+    for node_config in config.all_nodes_config_folders():
+        node_config_file = node_config / 'config.toml'
+        data = utils.read_toml_file(node_config_file)
+        node_config_toml.patch_config(data, config)
+        utils.write_toml_file(node_config_file, data)
+
+        api_config_file = node_config / 'api.toml'
+        data = utils.read_toml_file(api_config_file)
+        node_config_toml.patch_api(data, config)
+        utils.write_toml_file(api_config_file, data)
+
+        enable_epochs_config_file = node_config / 'enableEpochs.toml'
+        data = utils.read_toml_file(enable_epochs_config_file)
+        node_config_toml.patch_enable_epochs(data, config)
+        utils.write_toml_file(enable_epochs_config_file, data)
+
+        genesis_smart_contracts_file = node_config / 'genesisSmartContracts.json'
+        data = utils.read_json_file(genesis_smart_contracts_file)
+        genesis_smart_contracts_json.patch(data, config)
+        utils.write_json_file(genesis_smart_contracts_file, data)
+
+
+def copy_config_to_seednode(config: ConfigRoot):
+    config_source = config.software.mx_chain_go.get_seednode_config_folder()
+    seednode_config = config.seednode_config_folder()
+    makefolder(seednode_config)
+    shutil.copy(config_source / 'p2p.toml', seednode_config / 'p2p.toml')
+    shutil.copy(config_source / 'config.toml', seednode_config / 'config.toml')
+
+
+def patch_seednode_p2p_config(config: ConfigRoot):
+    seednode_config = config.seednode_config_folder()
+    seednode_config_file = seednode_config / 'p2p.toml'
+
+    data = utils.read_toml_file(seednode_config_file)
+    p2p_toml.patch_for_seednode(data, config)
+    utils.write_toml_file(seednode_config_file, data)
+
+
+def copy_seednode_p2p_key(config: ConfigRoot):
+    p2p_key_path = Path(__file__).parent / "seednode_p2pKey.pem"
+    shutil.copy(p2p_key_path, config.seednode_config_folder() / "p2pKey.pem")
+
+
+def patch_nodes_p2p_config(config: ConfigRoot, nodes_config_folders: List[Path], port_first: int):
+    for index, config_folder in enumerate(nodes_config_folders):
+        config_file = config_folder / 'p2p.toml'
+        data = utils.read_toml_file(config_file)
+        p2p_toml.patch(data, config, index, port_first)
+        utils.write_toml_file(config_file, data)
+
+
+def overwrite_nodes_setup(config: ConfigRoot, nodes_config_folders: List[Path]):
+    nodes_setup = nodes_setup_json.build(config)
+
+    for _, config_folder in enumerate(nodes_config_folders):
+        utils.write_json_file(config_folder / 'nodesSetup.json', nodes_setup)
+
+
+def overwrite_genesis_file(config: ConfigRoot, nodes_config_folders: List[Path]):
+    genesis = genesis_json.build(config)
+
+    for _, config_folder in enumerate(nodes_config_folders):
+        utils.write_json_file(config_folder / 'genesis.json', genesis)
+
+
+def copy_config_to_proxy(config: ConfigRoot):
+    config_prototype = config.software.mx_chain_proxy_go.get_proxy_config_folder()
+    proxy_config = config.proxy_config_folder()
+    makefolder(proxy_config)
+
+    shutil.copy(
+        config_prototype / 'config.toml',
+        proxy_config)
+
+    shutil.copytree(
+        config_prototype / 'apiConfig',
+        proxy_config / 'apiConfig')
+
+    shutil.copy(
+        config_prototype / 'external.toml',
+        proxy_config)
+
+
+def patch_proxy_config(config: ConfigRoot):
+    proxy_config_file = config.proxy_config_folder() / 'config.toml'
+    nodes = config.api_addresses_sharded_for_proxy_config()
+    data = utils.read_toml_file(proxy_config_file)
+    data['Observers'] = nodes
+    data['FullHistoryNodes'] = nodes
+    data['GeneralSettings']['ServerPort'] = config.networking.port_proxy
+    utils.write_toml_file(proxy_config_file, data)
+
+    api_config_file = path.join(config.proxy_config_folder(), 'apiConfig', 'v1_0.toml')
+    data = utils.read_toml_file(api_config_file)
+    routes = data['APIPackages']['transaction']['Routes']
+    for route in routes:
+        route["Open"] = True
+    utils.write_toml_file(api_config_file, data)
+
+
+def makefolder(path_where_to_make_folder: Path):
+    path_where_to_make_folder.mkdir(parents=True, exist_ok=True)
+
+
+def copy_binaries_into_localnet_workspace(config: ConfigRoot):
+    cmd_node = config.software.mx_chain_go.get_cmd_node_folder()
+    cmd_seednode = config.software.mx_chain_go.get_cmd_seednode_folder()
+    cmd_proxy = config.software.mx_chain_proxy_go.get_cmd_proxy_folder()
+
+    for destination in config.all_nodes_folders():
+        shutil.copy(cmd_node / "node", destination)
+        libraries.copy_libraries(cmd_node, destination)
+
+    shutil.copy(cmd_seednode / "seednode", config.seednode_folder())
+    libraries.copy_libraries(cmd_seednode, config.seednode_folder())
+
+    shutil.copy(cmd_proxy / "proxy", config.proxy_folder())
diff --git a/multiversx_sdk_cli/localnet/step_new.py b/multiversx_sdk_cli/localnet/step_new.py
new file mode 100644
index 00000000..7c1df9f4
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/step_new.py
@@ -0,0 +1,19 @@
+import logging
+from pathlib import Path
+
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+
+logger = logging.getLogger("localnet")
+
+
+def new_config(configfile: Path):
+    logger.info("new_config()")
+
+    configfile = configfile.expanduser().resolve()
+
+    if configfile.exists():
+        logger.warning(f"Configuration file already exists: {configfile}")
+        return
+
+    config = ConfigRoot()
+    config.save(configfile)
diff --git a/multiversx_sdk_cli/localnet/step_prerequisites.py b/multiversx_sdk_cli/localnet/step_prerequisites.py
new file mode 100644
index 00000000..c372e3ee
--- /dev/null
+++ b/multiversx_sdk_cli/localnet/step_prerequisites.py
@@ -0,0 +1,57 @@
+import logging
+import shutil
+import urllib.request
+from pathlib import Path
+
+from multiversx_sdk_cli import dependencies
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+from multiversx_sdk_cli.localnet.config_software import (SoftwareComponent,
+                                                         SoftwareResolution)
+
+logger = logging.getLogger("localnet")
+
+
+def fetch_prerequisites(configfile: Path):
+    logger.info("fetch_prerequisites()")
+
+    config = ConfigRoot.from_file(configfile)
+
+    dependencies.install_module("testwallets", tag="", overwrite=True)
+
+    if config.software.mx_chain_go.resolution == SoftwareResolution.Remote:
+        download_software_component(config.software.mx_chain_go)
+
+    if config.software.mx_chain_proxy_go.resolution == SoftwareResolution.Remote:
+        download_software_component(config.software.mx_chain_proxy_go)
+
+    config.software.mx_chain_go.node_config_must_exist()
+    config.software.mx_chain_go.seednode_config_must_exist()
+    config.software.mx_chain_proxy_go.proxy_config_must_exist()
+
+    is_node_built = config.software.mx_chain_go.is_node_built()
+    is_seednode_built = config.software.mx_chain_go.is_seednode_built()
+    is_proxy_built = config.software.mx_chain_proxy_go.is_proxy_built()
+
+    is_golang_needed = not (is_node_built and is_seednode_built and is_proxy_built)
+    if is_golang_needed:
+        dependencies.install_module("golang")
+
+
+def download_software_component(component: SoftwareComponent):
+    download_folder = component.get_archive_download_folder()
+    extraction_folder = component.get_archive_extraction_folder()
+    url = component.archive_url
+
+    shutil.rmtree(str(download_folder), ignore_errors=True)
+    shutil.rmtree(str(extraction_folder), ignore_errors=True)
+
+    download_folder.mkdir(parents=True, exist_ok=True)
+    extraction_folder.mkdir(parents=True, exist_ok=True)
+    archive_extension = url.split(".")[-1]
+    download_path = download_folder / f"archive.{archive_extension}"
+
+    logger.info(f"Downloading archive {url} to {download_path}")
+    urllib.request.urlretrieve(url, download_path)
+
+    logger.info(f"Unpacking archive {download_path} to {extraction_folder}")
+    shutil.unpack_archive(download_path, extraction_folder, format="zip")
diff --git a/multiversx_sdk_cli/testnet/core.py b/multiversx_sdk_cli/localnet/step_start.py
similarity index 52%
rename from multiversx_sdk_cli/testnet/core.py
rename to multiversx_sdk_cli/localnet/step_start.py
index bda6e4ec..14491576 100644
--- a/multiversx_sdk_cli/testnet/core.py
+++ b/multiversx_sdk_cli/localnet/step_start.py
@@ -1,12 +1,20 @@
 import asyncio
 import logging
+import os
+import sys
 import traceback
 from pathlib import Path
 from typing import Any, Coroutine, List
 
-from multiversx_sdk_cli.testnet.config import TestnetConfiguration
+from rich.console import Console
+from rich.table import Table
 
-logger = logging.getLogger("testnet")
+from multiversx_sdk_cli import workstation
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+from multiversx_sdk_cli.localnet.constants import \
+    NETWORK_MONITORING_INTERVAL_IN_SECONDS
+
+logger = logging.getLogger("localnet")
 
 
 NODES_START_DELAY = 1
@@ -15,74 +23,130 @@
 is_after_genesis = False
 
 
-def start(args: Any):
+def start(configfile: Path, stop_after_seconds: int):
+    logger.info("start()")
+
     try:
         loop = asyncio.get_event_loop()
-        loop.run_until_complete(do_start(args))
+        loop.run_until_complete(do_start(configfile, stop_after_seconds))
         loop.close()
         asyncio.set_event_loop(asyncio.new_event_loop())
     except KeyboardInterrupt:
         pass
 
 
-async def do_start(args: Any):
-    testnet_config = TestnetConfiguration.from_file(args.configfile)
-    logger.info('testnet folder is %s', testnet_config.root())
+async def do_start(configfile: Path, stop_after_seconds: int):
+    config = ConfigRoot.from_file(configfile)
+
+    display_api_table(config)
+
+    logger.info('Localnet folder is %s', config.root())
 
     to_run: List[Coroutine[Any, Any, None]] = []
 
     # Seed node
-    to_run.append(run(["./seednode", "--log-save"], cwd=testnet_config.seednode_folder()))
+    to_run.append(run([
+        "./seednode",
+        "--log-save",
+        f"--rest-api-interface={config.seednode_api_interface()}",
+    ], cwd=config.seednode_folder()))
 
-    loglevel = _patch_loglevel(testnet_config.loglevel())
+    loglevel = _patch_loglevel(config.general.log_level)
     logger.info(f"loglevel: {loglevel}")
 
     # Observers
-    for observer in testnet_config.observers():
+    for observer in config.observers():
         to_run.append(run([
             "./node",
             "--use-log-view",
             "--log-save",
             f"--log-level={loglevel}",
             "--log-logger-name",
-            "--log-correlation",
             f"--destination-shard-as-observer={observer.shard}",
-            f"--rest-api-interface=localhost:{observer.api_port}"
+            f"--rest-api-interface={observer.api_interface()}"
         ], cwd=observer.folder, delay=NODES_START_DELAY))
 
     # Validators
-    for validator in testnet_config.validators():
+    for validator in config.validators():
         to_run.append(run([
             "./node",
             "--use-log-view",
             "--log-save",
             f"--log-level={loglevel}",
             "--log-logger-name",
-            "--log-correlation",
-            f"--rest-api-interface=localhost:{validator.api_port}"
+            f"--rest-api-interface={validator.api_interface()}"
         ], cwd=validator.folder, delay=NODES_START_DELAY))
 
     # Proxy
     to_run.append(run([
         "./proxy",
         "--log-save"
-    ], cwd=testnet_config.proxy_folder(), delay=PROXY_START_DELAY))
+    ], cwd=config.proxy_folder(), delay=PROXY_START_DELAY))
+
+    # Monitor network
+    to_run.append(monitor_network(stop_after_seconds))
+
+    tasks = [asyncio.create_task(item) for item in to_run]
+    await asyncio.gather(*tasks)
+
+
+def display_api_table(config: ConfigRoot):
+    table = Table(title="APIs")
+
+    table.add_column("Type")
+    table.add_column("Shard")
+    table.add_column("URL")
+
+    table.add_row("Proxy", "*", config.networking.get_proxy_url())
+
+    for observer in config.observers():
+        table.add_row("Observer", observer.shard, observer.api_address())
 
-    await asyncio.gather(*to_run)
+    for validator in config.validators():
+        table.add_row("Validator", validator.shard, validator.api_address())
+
+    table.add_row("Seednode", "N / A", config.seednode_api_address())
+
+    console = Console()
+    console.print(table)
+
+
+async def monitor_network(stop_after_seconds: int):
+    loop = asyncio.get_running_loop()
+    end_time = loop.time() + stop_after_seconds
+
+    while True:
+        current_loop_time = loop.time()
+
+        if current_loop_time >= end_time:
+            loop.stop()
+            sys.exit(0)
+
+        await asyncio.sleep(NETWORK_MONITORING_INTERVAL_IN_SECONDS)
 
 
 async def run(args: List[str], cwd: Path, delay: int = 0):
     await asyncio.sleep(delay)
 
+    logger.info(f"Starting process {args} in folder {cwd}")
+
+    env = os.environ.copy()
+
+    if workstation.is_linux():
+        env["LD_LIBRARY_PATH"] = str(cwd)
+    else:
+        # For MacOS, libwasmer is directly found near the binary (no workaround needed)
+        pass
+
     process = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE,
-                                                   stderr=asyncio.subprocess.PIPE, cwd=cwd, limit=1024 * 512)
+                                                   stderr=asyncio.subprocess.PIPE, cwd=cwd, limit=1024 * 512, env=env)
 
     pid = process.pid
 
     print(f"Started process [{pid}]", args)
     await asyncio.wait([
-        _read_stream(process.stdout, pid),
-        _read_stream(process.stderr, pid)
+        asyncio.create_task(_read_stream(process.stdout, pid)),
+        asyncio.create_task(_read_stream(process.stderr, pid))
     ])
 
     return_code = await process.wait()
diff --git a/multiversx_sdk_cli/testnet/wallets.py b/multiversx_sdk_cli/localnet/wallets.py
similarity index 87%
rename from multiversx_sdk_cli/testnet/wallets.py
rename to multiversx_sdk_cli/localnet/wallets.py
index d71d37a5..2134e889 100644
--- a/multiversx_sdk_cli/testnet/wallets.py
+++ b/multiversx_sdk_cli/localnet/wallets.py
@@ -3,9 +3,10 @@
 from pathlib import Path
 from typing import Dict, Tuple
 
+from multiversx_sdk_wallet.validator_pem import ValidatorPEM
+
 from multiversx_sdk_cli import errors, utils
 from multiversx_sdk_cli.accounts import Account
-from multiversx_sdk_wallet.validator_pem import ValidatorPEM
 from multiversx_sdk_cli.workstation import get_tools_folder
 
 MAX_NUM_NODES = 12
@@ -30,19 +31,19 @@ def get_observer_key_file(observer_index: int):
 
 
 def get_validator_wallets(num_validators: int) -> Dict[str, Account]:
-    result = {}
+    result: Dict[str, Account] = {}
 
     for i in range(0, num_validators):
         pem_file = get_validator_wallet_file(i)
         nickname = "validator{:02}".format(i)
-        account = Account(pem_file=pem_file)
+        account = Account(pem_file=str(pem_file))
         result[nickname] = account
 
     return result
 
 
 def get_validators(num_validators: int) -> Dict[str, Tuple[str, Account]]:
-    result = {}
+    result: Dict[str, Tuple[str, Account]] = {}
 
     for i in range(0, num_validators):
         validator_pem_file = get_validator_key_file(i)
@@ -51,7 +52,7 @@ def get_validators(num_validators: int) -> Dict[str, Tuple[str, Account]]:
 
         pem_file = get_validator_wallet_file(i)
         nickname = "validator{:02}".format(i)
-        account = Account(pem_file=pem_file)
+        account = Account(pem_file=str(pem_file))
         result[nickname] = (pubkey, account)
 
     return result
@@ -64,7 +65,7 @@ def get_validator_wallet_file(validator_index: int):
 
 def _guard_validator_index(validator_index: int):
     if validator_index >= MAX_NUM_NODES:
-        raise errors.TestnetError(f"Invalid validator index: {validator_index} >= {MAX_NUM_NODES}.")
+        raise errors.KnownError(f"Invalid validator index: {validator_index} >= {MAX_NUM_NODES}.")
 
 
 def _get_validators_folder():
@@ -91,4 +92,4 @@ def _get_users_folder():
 
 
 def _get_folder():
-    return get_tools_folder() / "testwallets" / "latest" 
+    return get_tools_folder() / "testwallets" / "latest"
diff --git a/multiversx_sdk_cli/projects/eei_activation.py b/multiversx_sdk_cli/projects/eei_activation.py
deleted file mode 100644
index 64afa910..00000000
--- a/multiversx_sdk_cli/projects/eei_activation.py
+++ /dev/null
@@ -1,39 +0,0 @@
-
-import logging
-import sys
-
-import requests
-import toml
-from multiversx_sdk_cli.diskcache import DiskCache
-from multiversx_sdk_network_providers.proxy_network_provider import ProxyNetworkProvider
-
-logger = logging.getLogger("eei")
-
-
-class ActivationEpochsInfo(DiskCache):
-    def __init__(self, network_name: str, proxy_url: str, enable_epochs_url: str) -> None:
-        super().__init__(cache_name="projects.eei.ActivationEpochsInfo", max_age=60 * 30)
-        self.network_name = network_name
-        self.proxy_url = proxy_url
-        self.enable_epochs_url = enable_epochs_url
-
-    def is_flag_active(self, flag_name: str):
-        current_epoch_key = f"epoch:{self.proxy_url}"
-        enable_epochs_key = f"config:{self.enable_epochs_url}"
-
-        current_epoch = self.get_and_cache_item(current_epoch_key, self._fetch_current_epoch)
-        enable_epochs = self.get_and_cache_item(enable_epochs_key, self._fetch_enable_epochs)
-        enable_epoch = enable_epochs.get(flag_name, sys.maxsize)
-        return current_epoch >= enable_epoch
-
-    def _fetch_current_epoch(self):
-        logger.info(f"fetch_current_epoch: {self.proxy_url}")
-        proxy = ProxyNetworkProvider(self.proxy_url)
-        return proxy.get_network_status().epoch_number
-
-    def _fetch_enable_epochs(self):
-        logger.info(f"fetch_enable_epochs: {self.enable_epochs_url}")
-        response = requests.get(self.enable_epochs_url)
-        response.raise_for_status()
-        enable_epochs = toml.loads(response.text).get("EnableEpochs", dict())
-        return dict(enable_epochs)
diff --git a/multiversx_sdk_cli/projects/eei_checks.py b/multiversx_sdk_cli/projects/eei_checks.py
deleted file mode 100644
index 1c1f11e3..00000000
--- a/multiversx_sdk_cli/projects/eei_checks.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import logging
-from typing import List
-
-from multiversx_sdk_cli import utils
-from multiversx_sdk_cli.projects.eei_activation import ActivationEpochsInfo
-from multiversx_sdk_cli.projects.eei_registry import EEIRegistry
-from multiversx_sdk_cli.projects.interfaces import IProject
-
-logger = logging.getLogger("eei")
-
-MAINNET_PROXY_URL = "https://gateway.multiversx.com"
-MAINNET_ENABLE_EPOCHS_URL = "https://raw.githubusercontent.com/multiversx/mx-chain-mainnet-config/master/enableEpochs.toml"
-DEVNET_PROXY_URL = "https://devnet-gateway.multiversx.com"
-DEVNET_ENABLE_EPOCHS_URL = "https://raw.githubusercontent.com/multiversx/mx-chain-devnet-config/master/enableEpochs.toml"
-
-
-def check_compatibility(project: IProject):
-    if _should_skip_checks(project):
-        return
-
-    logger.info("check_compatibility")
-
-    wasm_file = project.get_file_wasm()
-    imports_file = wasm_file.with_suffix(".imports.json")
-    imports = utils.read_json_file(imports_file)
-
-    activation_info_on_mainnet = ActivationEpochsInfo("mainnet", MAINNET_PROXY_URL, MAINNET_ENABLE_EPOCHS_URL)
-    activation_info_on_devnet = ActivationEpochsInfo("devnet", DEVNET_PROXY_URL, DEVNET_ENABLE_EPOCHS_URL)
-    compatible_with_mainnet = _check_imports_compatibility(imports, activation_info_on_mainnet)
-    compatible_with_devnet = _check_imports_compatibility(imports, activation_info_on_devnet)
-
-    if not compatible_with_mainnet or not compatible_with_devnet:
-        logger.warn("Some features used by the project are not yet completely supported.")
-
-
-def _should_skip_checks(project: IProject):
-    return not project.get_option("eei-checks")
-
-
-def _check_imports_compatibility(imports: List[str], activation_info: ActivationEpochsInfo) -> bool:
-    registry = EEIRegistry(activation_info)
-    registry.sync_flags()
-
-    not_active: List[str] = []
-    not_active_maybe: List[str] = []
-
-    for function_name in imports:
-        is_active = registry.is_function_active(function_name)
-        if is_active is False:
-            not_active.append(function_name)
-        elif is_active is None:
-            not_active_maybe.append(function_name)
-
-    if not_active:
-        logger.error(f"This project requires functionality not yet available on *{activation_info.network_name}*: {not_active}. Use --ignore-eei-checks to ignore this error.")
-    if not_active_maybe:
-        logger.warn(f"This project requires functionality that may not be available on *{activation_info.network_name}*: {not_active_maybe}.")
-
-    fully_compatible = len(not_active + not_active_maybe) == 0
-    return fully_compatible
diff --git a/multiversx_sdk_cli/projects/eei_registry.py b/multiversx_sdk_cli/projects/eei_registry.py
deleted file mode 100644
index 37a37f0c..00000000
--- a/multiversx_sdk_cli/projects/eei_registry.py
+++ /dev/null
@@ -1,47 +0,0 @@
-import logging
-from typing import List, Union
-
-from multiversx_sdk_cli.projects.eei_activation import ActivationEpochsInfo
-
-
-class EEIRegistry:
-    def __init__(self, activation_info: ActivationEpochsInfo) -> None:
-        self.activation_info = activation_info
-        self.flags: List[FeatureFlag] = []
-        self.functions: List[EEIFunction] = [
-            # e.g. EEIFunction("foobar", FeatureFlag("fooFeature"), []),
-        ]
-        self.functions_dict = {function.name: function for function in self.functions}
-
-    def sync_flags(self):
-        for flag in self.flags:
-            flag.sync(self.activation_info)
-
-    def is_function_active(self, function_name: str) -> Union[bool, None]:
-        function = self.functions_dict.get(function_name, None)
-        if function is None:
-            # If function is not found in this registry, assume it is <active> on all known networks.
-            return True
-        if function.activated_by is None:
-            return True
-        return function.activated_by.is_active
-
-
-class FeatureFlag:
-    def __init__(self, name: str) -> None:
-        self.name = name
-        self.is_active: Union[bool, None] = None
-
-    def sync(self, activation_info: ActivationEpochsInfo):
-        try:
-            self.is_active = activation_info.is_flag_active(self.name)
-        except Exception as err:
-            self.is_active = None
-            logging.error(err)
-
-
-class EEIFunction:
-    def __init__(self, name: str, activated_by: Union[FeatureFlag, None], tags: List[str]) -> None:
-        self.name = name
-        self.activated_by = activated_by
-        self.tags = tags
diff --git a/multiversx_sdk_cli/projects/project_base.py b/multiversx_sdk_cli/projects/project_base.py
index 470850f5..01724225 100644
--- a/multiversx_sdk_cli/projects/project_base.py
+++ b/multiversx_sdk_cli/projects/project_base.py
@@ -8,7 +8,6 @@
 
 from multiversx_sdk_cli import dependencies, errors, myprocess, utils
 from multiversx_sdk_cli.dependencies.modules import StandaloneModule
-from multiversx_sdk_cli.projects import eei_checks
 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
@@ -83,8 +82,7 @@ def _do_after_build_custom(self) -> List[Path]:
 
     @final
     def _do_after_build_core(self):
-        # TODO: Remove this, in the future
-        eei_checks.check_compatibility(self)
+        pass
 
     def _copy_to_output(self, source: Path, destination: Union[str, None] = None) -> Path:
         output_folder = self.get_output_folder()
diff --git a/multiversx_sdk_cli/utils.py b/multiversx_sdk_cli/utils.py
index 1cdd8818..751aa25b 100644
--- a/multiversx_sdk_cli/utils.py
+++ b/multiversx_sdk_cli/utils.py
@@ -107,11 +107,11 @@ def write_file(file_path: Path, text: str):
         return file.write(text)
 
 
-def read_toml_file(filename):
+def read_toml_file(filename: Union[str, Path]):
     return toml.load(str(filename))
 
 
-def write_toml_file(filename, data):
+def write_toml_file(filename: Union[str, Path], data: Any):
     with open(str(filename), "w") as f:
         toml.dump(data, f)
 
@@ -121,8 +121,8 @@ def read_json_file(filename: Union[str, Path]) -> Any:
         return json.load(f)
 
 
-def write_json_file(filename: str, data: Any):
-    with open(filename, "w") as f:
+def write_json_file(filename: Union[str, Path], data: Any):
+    with open(str(filename), "w") as f:
         json.dump(data, f, indent=4)
 
 
diff --git a/multiversx_sdk_cli/ux.py b/multiversx_sdk_cli/ux.py
new file mode 100644
index 00000000..dafbadd9
--- /dev/null
+++ b/multiversx_sdk_cli/ux.py
@@ -0,0 +1,10 @@
+from rich import print
+from rich.panel import Panel
+
+
+def show_message(message: str):
+    print(Panel(f"[green]{message}"))
+
+
+def show_critical_error(message: str):
+    print(Panel(f"[red]{message}"))
diff --git a/multiversx_sdk_cli/workstation.py b/multiversx_sdk_cli/workstation.py
index 0056c571..01996d9d 100644
--- a/multiversx_sdk_cli/workstation.py
+++ b/multiversx_sdk_cli/workstation.py
@@ -10,6 +10,14 @@ def get_tools_folder() -> Path:
     return folder
 
 
+def is_linux():
+    return get_platform() == "linux"
+
+
+def is_windows():
+    return get_platform() == "windows"
+
+
 def get_platform():
     platforms = {
         "linux": "linux",
diff --git a/pyproject.toml b/pyproject.toml
index 40ca4b59..5402fccf 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "multiversx-sdk-cli"
-version = "6.5.0"
+version = "7.0.0"
 authors = [
   { name="MultiversX" },
 ]
@@ -25,9 +25,10 @@ dependencies = [
   "ledgercomm[hid]",
   "semver",
   "requests-cache",
+  "rich==13.3.4",
   "multiversx-sdk-network-providers==0.6.*",
   "multiversx-sdk-wallet==0.7.*",
-  "multiversx-sdk-core>=0.4.1"
+  "multiversx-sdk-core>=0.5.0"
 ]
 
 [tool.hatch.build]
diff --git a/requirements.txt b/requirements.txt
index c0ce6c99..214bd039 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,7 +7,8 @@ types-prettytable
 ledgercomm[hid]
 semver
 requests-cache
+rich==13.3.4
 
-multiversx-sdk-core>=0.4.1
+multiversx-sdk-core>=0.5.0
 multiversx-sdk-network-providers==0.6.*
 multiversx-sdk-wallet==0.7.*


Test patch:
diff --git a/.github/workflows/test-localnet.yml b/.github/workflows/test-localnet.yml
new file mode 100644
index 00000000..fc52ee2c
--- /dev/null
+++ b/.github/workflows/test-localnet.yml
@@ -0,0 +1,67 @@
+name: Test localnet
+
+on:
+  pull_request:
+    branches: [main, feat/*]
+  workflow_dispatch:
+
+env:
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
+
+jobs:
+  localnet:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [ubuntu-latest, macos-latest]
+        python-version: [3.11]
+
+    steps:
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+
+      - name: Install dependencies
+        run: |
+          python3 -m pip install --upgrade pip
+          pip3 install -r requirements.txt
+
+      - name: Smoke test (with resolution == remote)
+        run: |
+          mkdir -p ~/multiversx-sdk
+          export PYTHONPATH=.
+          python3 -m multiversx_sdk_cli.cli config set github_api_token ${{ secrets.GITHUB_TOKEN }}
+          python3 -m multiversx_sdk_cli.cli localnet prerequisites --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml
+          python3 -m multiversx_sdk_cli.cli localnet build --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml
+         
+          # "Go" and artifacts from "GOPATH/pkg/mod" are not needed anymore.
+          sudo rm -rf ~/multiversx-sdk/golang
+          
+          python3 -m multiversx_sdk_cli.cli localnet clean --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml
+          python3 -m multiversx_sdk_cli.cli localnet config --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml
+          python3 -m multiversx_sdk_cli.cli localnet start --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml --stop-after-seconds=120
+
+      - name: Smoke test (with resolution == local)
+        run: |
+          mkdir -p ~/multiversx-sdk/sandbox
+          export PYTHONPATH=.
+
+          git clone https://github.com/multiversx/mx-chain-go --branch=master --single-branch ~/multiversx-sdk/sandbox/mx-chain-go
+          git clone https://github.com/multiversx/mx-chain-proxy-go --branch=master --single-branch ~/multiversx-sdk/sandbox/mx-chain-proxy-go
+          
+          python3 -m multiversx_sdk_cli.cli localnet prerequisites --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml
+          python3 -m multiversx_sdk_cli.cli localnet build --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml
+         
+          # "Go" and artifacts from "GOPATH/pkg/mod" are not needed anymore.
+          sudo rm -rf ~/multiversx-sdk/golang
+          
+          python3 -m multiversx_sdk_cli.cli localnet clean --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml
+          python3 -m multiversx_sdk_cli.cli localnet config --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml
+          python3 -m multiversx_sdk_cli.cli localnet start --configfile=./multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml --stop-after-seconds=120
diff --git a/multiversx_sdk_cli/cli_testnet.py b/multiversx_sdk_cli/cli_testnet.py
deleted file mode 100644
index d916ab19..00000000
--- a/multiversx_sdk_cli/cli_testnet.py
+++ /dev/null
@@ -1,81 +0,0 @@
-import logging
-from typing import Any, List
-
-from multiversx_sdk_cli import cli_shared, testnet
-
-logger = logging.getLogger("cli.testnet")
-
-
-def setup_parser(args: List[str], subparsers: Any) -> Any:
-    parser = cli_shared.add_group_subparser(
-        subparsers,
-        "testnet",
-        "Set up, start and control local testnets"
-    )
-    subparsers = parser.add_subparsers()
-
-    help_config_file = "An optional configuration file describing the testnet"
-
-    # Prerequisites
-    sub = cli_shared.add_command_subparser(
-        subparsers,
-        "testnet",
-        "prerequisites",
-        "Download and verify the prerequisites for running a testnet"
-    )
-    sub.add_argument("--configfile", type=str, required=False, default=None,
-                     help=help_config_file)
-    sub.set_defaults(func=testnet_prerequisites)
-
-    # Start
-    sub = cli_shared.add_command_subparser(
-        subparsers,
-        "testnet",
-        "start",
-        "Start a testnet"
-    )
-    sub.add_argument("--configfile", type=str, required=False, default=None,
-                     help=help_config_file)
-    sub.set_defaults(func=testnet_start)
-
-    # Config
-    sub = cli_shared.add_command_subparser(
-        subparsers,
-        "testnet",
-        "config",
-        "Configure a testnet (required before starting it the first time or after clean)"
-    )
-    sub.add_argument("--configfile", type=str, required=False, default=None,
-                     help=help_config_file)
-    sub.set_defaults(func=testnet_config)
-
-    # Clean
-    sub = cli_shared.add_command_subparser(
-        subparsers,
-        "testnet",
-        "clean",
-        "Erase the currently configured testnet (must be already stopped)"
-    )
-    sub.add_argument("--configfile", type=str, required=False, default=None,
-                     help=help_config_file)
-    sub.set_defaults(func=testnet_clean)
-
-
-def testnet_start(args: Any):
-    logger.info("Starting testnet...")
-    testnet.start(args)
-
-
-def testnet_config(args: Any):
-    logger.info("Configuring testnet...")
-    testnet.configure(args)
-
-
-def testnet_clean(args: Any):
-    logger.info("Cleaning testnet...")
-    testnet.clean(args)
-
-
-def testnet_prerequisites(args):
-    logger.info("Preparing prerequisites...")
-    testnet.install_dependencies()
diff --git a/multiversx_sdk_cli/testnet/__init__.py b/multiversx_sdk_cli/testnet/__init__.py
deleted file mode 100644
index 1daf0a7c..00000000
--- a/multiversx_sdk_cli/testnet/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-from multiversx_sdk_cli.testnet.core import start
-from multiversx_sdk_cli.testnet.setup import clean, configure, install_dependencies
-
-__all__ = ["install_dependencies", "configure", "clean", "start"]
diff --git a/multiversx_sdk_cli/testnet/config.py b/multiversx_sdk_cli/testnet/config.py
deleted file mode 100644
index 8d71073b..00000000
--- a/multiversx_sdk_cli/testnet/config.py
+++ /dev/null
@@ -1,341 +0,0 @@
-import itertools
-import logging
-import time
-from pathlib import Path
-from typing import Any, Dict, Iterator
-
-import multiversx_sdk_cli.config
-import multiversx_sdk_cli.utils as utils
-from multiversx_sdk_cli import workstation
-
-logger = logging.getLogger("testnet")
-
-ConfigurationDict = Dict[str, Dict[str, Any]]
-
-
-class Node:
-    def __init__(self, index: int, folder: Path, shard: str, api_port: int) -> None:
-        self.index = index
-        self.folder = folder
-        self.shard = shard
-        self.api_port = api_port
-
-    def config_folder(self):
-        return self.folder / 'config'
-
-    def key_file_path(self):
-        return self.config_folder() / 'validatorKey.pem'
-
-    def api_address(self):
-        return f"http://localhost:{self.api_port}"
-
-    def __repr__(self) -> str:
-        return f"Node {self.index}, shard={self.shard}, port={self.api_port}, folder={self.folder}"
-
-
-class TestnetConfiguration:
-    config: ConfigurationDict
-    folders: Dict[str, Path]
-    networking: Dict[str, Any]
-    metashard: Dict[str, Any]
-    shards: Dict[str, Any]
-    features: Dict[str, Any]
-    timing: Dict[str, Any]
-
-    def __init__(self, config):
-        self.config = config
-        self.folders = dict()
-        self.networking = dict()
-        self.metashard = dict()
-        self.shards = dict()
-        self.features = dict()
-        self.timing = dict()
-
-        sdk_folder = workstation.get_tools_folder()
-
-        for folder_key, folder_path in self.config['folders'].items():
-            if folder_key == 'testnet':
-                continue
-
-            folder_path = folder_path.replace('{MULTIVERSX_SDK}', str(sdk_folder))
-
-            default_tag = multiversx_sdk_cli.config.get_dependency_tag(folder_key)
-            if default_tag == 'latest':
-                parent_folder = multiversx_sdk_cli.config.get_dependency_parent_directory(folder_key)
-                default_tag = multiversx_sdk_cli.config.get_latest_semver_from_directory(parent_folder)
-
-            folder_path = folder_path.replace('{TAG}', default_tag)
-
-            # If the user has not specified a custom source repository, the
-            # ones provided by the SDK will be used, which are downloaded as
-            # tar.gz files from GitHub. Due to how the name of the archive is
-            # built by GitHub, the path will contain the tag in two variants:
-            # with the 'v' prefix (e.g. "v1.1.0"), but also without (e.g.
-            # "1.1.0"), hence the need for {NOvTAG}.
-            if default_tag.startswith('v'):
-                default_tag = default_tag[1:]
-            folder_path = folder_path.replace('{NOvTAG}', default_tag)
-
-            self.folders[folder_key] = Path(folder_path).expanduser()
-
-        try:
-            self.folders['testnet'] = Path(self.config['folders']['testnet']).expanduser()
-        except KeyError:
-            self.folders['testnet'] = Path().absolute() / 'testnet'
-
-        self.networking.update(self.config.get('networking', dict()))
-        self.metashard.update(self.config.get('metashard', dict()))
-        self.shards.update(self.config.get('shards', dict()))
-        self.features.update(self.config.get('features', dict()))
-        self.timing.update(self.config.get('timing', dict()))
-
-    @classmethod
-    def from_file(cls, filename: str):
-        """
-        If no filename is specified, try to load testnet.toml from the current
-        directory, if there is any, and merge it with the SDK-level testnet
-        config. Otherwise, load the SDK-level testnet config. If that one is
-        missing as well, generate it from hard-coded defaults.
-        """
-
-        local_config = cls.get_local_config(filename)
-        final_config = merge_configs(cls.default(), local_config)
-        return cls(final_config)
-
-    @classmethod
-    def from_default_config(cls):
-        return cls(cls.default())
-
-    @classmethod
-    def get_local_config(cls, filename):
-        if filename is None or filename == '':
-            filename = Path() / "testnet.toml"
-            if not filename.exists():
-                return dict()
-
-        return utils.read_toml_file(filename)
-
-    def node_config_source(self) -> Path:
-        return self.node_source() / 'cmd' / 'node' / 'config'
-
-    def node_source(self) -> Path:
-        path = self.folders['mx_chain_go']
-        assert isinstance(path, Path)
-        return path
-
-    def proxy_source(self) -> Path:
-        return self.folders['mx_chain_proxy_go']
-
-    def proxy_config_source(self):
-        return self.folders['mx_chain_proxy_go'] / 'cmd' / 'proxy' / 'config'
-
-    def validator_key_files(self):
-        for config_folder in self.validator_config_folders():
-            yield config_folder / 'validatorKey.pem'
-
-    def root(self):
-        return self.folders['testnet']
-
-    def genesis_time(self):
-        return int(time.time()) + int(self.timing['genesis_delay'])
-
-    def seednode_folder(self):
-        testnet = self.root()
-        return testnet / 'seednode'
-
-    def seednode_config_folder(self):
-        return self.seednode_folder() / 'config'
-
-    def seednode_address(self):
-        host = self.networking['host']
-        port = self.networking['port_seednode']
-        identifier = self.networking['p2p_id_seednode']
-        return f"/ip4/{host}/tcp/{port}/p2p/{identifier}"
-
-    def proxy_folder(self):
-        testnet = self.root()
-        return testnet / 'proxy'
-
-    def proxy_config_folder(self):
-        return self.proxy_folder() / 'config'
-
-    def num_all_nodes(self):
-        return self.num_all_validators() + self.num_all_observers()
-
-    def num_all_validators(self):
-        count = (
-            self.shards['count'] * self.shards['validators_per_shard'] + self.metashard['validators']
-        )
-        return count
-
-    def num_all_observers(self):
-        count = (
-            self.shards['count'] * self.shards['observers_per_shard'] + self.metashard['observers']
-        )
-        return count
-
-    def num_shards(self):
-        return self.shards['count']
-
-    def num_observers_per_shard(self):
-        return self.shards['observers_per_shard']
-
-    def num_validators_per_shard(self):
-        return self.shards['validators_per_shard']
-
-    def num_validators_in_metashard(self):
-        return self.metashard['validators']
-
-    def all_nodes_folders(self):
-        return itertools.chain(
-            self.validator_folders(),
-            self.observer_folders(),
-        )
-
-    def all_nodes_config_folders(self):
-        return itertools.chain(
-            self.validator_config_folders(),
-            self.observer_config_folders(),
-        )
-
-    def validators(self) -> Iterator[Node]:
-        first_port = self.networking['port_first_validator_rest_api']
-
-        for i, folder in enumerate(self.validator_folders()):
-            shard = self._get_shard_of_validator(i)
-            port = first_port + i
-            yield Node(index=i, folder=folder, shard=shard, api_port=port)
-
-    def _get_shard_of_validator(self, observer_index: int):
-        shard = int(observer_index // self.num_validators_per_shard())
-        return shard if shard < self.num_shards() else self.metashard['metashardID']
-
-    def validator_folders(self):
-        testnet = self.root()
-        for i in range(self.num_all_validators()):
-            yield testnet / 'validator{:02}'.format(i)
-
-    def validator_config_folders(self):
-        for folder in self.validator_folders():
-            yield folder / 'config'
-
-    def observers(self) -> Iterator[Node]:
-        first_port = self.networking['port_first_observer_rest_api']
-
-        for i, folder in enumerate(self.observer_folders()):
-            shard = self._get_shard_of_observer(i)
-            api_port = first_port + i
-            yield Node(index=i, folder=folder, shard=shard, api_port=api_port)
-
-    def _get_shard_of_observer(self, observer_index: int):
-        shard = int(observer_index // self.num_observers_per_shard())
-        return shard if shard < self.num_shards() else self.metashard['metashardID']
-
-    def observer_config_folders(self):
-        for folder in self.observer_folders():
-            yield folder / 'config'
-
-    def observer_folders(self):
-        testnet = self.root()
-        for i in range(self.num_all_observers()):
-            yield testnet / 'observer{:02}'.format(i)
-
-    def observer_addresses(self):
-        host = self.networking['host']
-        first_port = self.networking['port_first_observer_rest_api']
-        for port in range(self.num_all_observers()):
-            port = first_port + port
-            yield f"http://{host}:{port}"
-
-    def validator_addresses(self):
-        host = self.networking['host']
-        first_port = self.networking['port_first_observer_rest_api']
-        for port in range(self.num_all_observers()):
-            port = first_port + port
-            yield f"http://{host}:{port}"
-
-    def api_addresses_sharded_for_proxy_config(self):
-        nodes = list()
-        for node in list(self.observers()):
-            nodes.append({
-                'ShardId': node.shard,
-                'Address': node.api_address(),
-                'Type': 'Observer'
-            })
-
-        for node in list(self.validators()):
-            nodes.append({
-                'ShardId': node.shard,
-                'Address': node.api_address(),
-                'Type': 'Validator'
-            })
-
-        return nodes
-
-    def proxy_port(self):
-        return self.networking["port_proxy"]
-
-    def loglevel(self) -> str:
-        return self.features.get("loglevel", "")
-
-    def wasm_vm_binary(self) -> bool:
-        return self.features.get("wasm_vm_binary", False)
-
-    @classmethod
-    def default(cls):
-        config = dict()
-        config['features'] = {
-            'loglevel': '*:DEBUG',
-            'proxy': True,
-            'wasm_vm_binary': False
-        }
-        config['folders'] = {
-            'mx_chain_go':
-                '{MULTIVERSX_SDK}/mx_chain_go/{TAG}/mx-chain-go-{NOvTAG}',
-            'mx_chain_proxy_go':
-                '{MULTIVERSX_SDK}/mx_chain_proxy_go/{TAG}/mx-chain-proxy-go-{NOvTAG}',
-        }
-        config['metashard'] = {
-            'consensus_size': 1,
-            'metashardID': 4294967295,
-            'observers': 0,
-            'validators': 1,
-        }
-        config['networking'] = {
-            'host': '127.0.0.1',
-            'port_seednode': 9999,
-            'p2p_id_seednode': '16Uiu2HAkx4QqgXXDdHdUWbLu5kxhd3Uo2hqB2FfCxmxH5Sd7bZFk',
-            'port_proxy': 7950,
-            'port_first_observer': 21100,
-            'port_first_observer_rest_api': 10000,
-            'port_first_validator': 21500,
-            'port_first_validator_rest_api': 10100,
-        }
-        config['shards'] = {
-            'consensus_size': 1,
-            'count': 1,
-            'observers_per_shard': 0,
-            'validators_per_shard': 1,
-        }
-        config['timing'] = {
-            'genesis_delay': 10
-        }
-
-        return config
-
-
-def merge_configs(leftcfg: ConfigurationDict, rightcfg: ConfigurationDict) -> ConfigurationDict:
-    result = dict()
-    result.update(leftcfg)
-
-    for key, subcfg in result.items():
-        try:
-            subcfg.update(rightcfg[key])
-        except KeyError:
-            pass
-
-    for key in rightcfg:
-        if key not in result:
-            result[key] = rightcfg[key]
-
-    return result
diff --git a/multiversx_sdk_cli/testnet/node_config_toml.py b/multiversx_sdk_cli/testnet/node_config_toml.py
deleted file mode 100644
index bdedf303..00000000
--- a/multiversx_sdk_cli/testnet/node_config_toml.py
+++ /dev/null
@@ -1,138 +0,0 @@
-from typing import Dict, Any
-
-import multiversx_sdk_cli.config
-
-from multiversx_sdk_cli.testnet.config import TestnetConfiguration
-from multiversx_sdk_cli.testnet.nodes_setup_json import CHAIN_ID
-
-ConfigDict = Dict[str, Any]
-
-
-def patch_config(data: ConfigDict, testnet_config: TestnetConfiguration):
-    data['DbLookupExtensions']['Enabled'] = True
-
-    general_settings: ConfigDict = dict()
-    general_settings['ChainID'] = CHAIN_ID
-    general_settings['MinTransactionVersion'] = 1
-    general_settings['StartInEpochEnabled'] = True
-
-    data['GeneralSettings'].update(general_settings)
-
-    # Make epochs shorter - never below 100 rounds
-    epoch_start_config: ConfigDict = dict()
-    epoch_start_config['RoundsPerEpoch'] = 100
-    epoch_start_config['MinRoundsBetweenEpochs'] = 20
-    epoch_start_config['MinShuffledOutRestartThreshold'] = 0.1
-    epoch_start_config['MaxShuffledOutRestartThreshold'] = 0.5
-
-    data['EpochStartConfig'].update(epoch_start_config)
-
-    # Always use the latest VM
-    virtual_machine: Dict[str, Any] = dict()
-    virtual_machine['Execution'] = dict()
-    virtual_machine['Execution']['WasmVMVersions'] = [{'StartEpoch': 0, 'Version': '*'}]
-    virtual_machine['Querying'] = dict()
-    virtual_machine['Querying']['NumConcurrentVMs'] = 1
-    virtual_machine['Querying']['WasmVMVersions'] = [{'StartEpoch': 0, 'Version': '*'}]
-
-    data['VirtualMachine'].update(virtual_machine)
-
-
-def patch_api(data: ConfigDict, testnet_config: TestnetConfiguration):
-    routes = data['APIPackages']['transaction']['Routes']
-    for route in routes:
-        route['Open'] = True
-
-
-def patch_enable_epochs(data: ConfigDict, testnet_config: TestnetConfiguration):
-    enable_epochs: ConfigDict = dict()
-    enable_epochs['SCDeployEnableEpoch'] = 0
-    enable_epochs['BuiltInFunctionsEnableEpoch'] = 0
-    enable_epochs['RelayedTransactionsEnableEpoch'] = 0
-    enable_epochs['PenalizedTooMuchGasEnableEpoch'] = 0
-    enable_epochs['SwitchJailWaitingEnableEpoch'] = 0
-    enable_epochs['BelowSignedThresholdEnableEpoch'] = 0
-    enable_epochs['SwitchHysteresisForMinNodesEnableEpoch'] = 1
-    enable_epochs['TransactionSignedWithTxHashEnableEpoch'] = 1
-    enable_epochs['MetaProtectionEnableEpoch'] = 1
-    enable_epochs['AheadOfTimeGasUsageEnableEpoch'] = 0
-    enable_epochs['GasPriceModifierEnableEpoch'] = 0
-    enable_epochs['RepairCallbackEnableEpoch'] = 0
-    enable_epochs['BlockGasAndFeesReCheckEnableEpoch'] = 0
-    enable_epochs['ReturnDataToLastTransferEnableEpoch'] = 0
-    enable_epochs['SenderInOutTransferEnableEpoch'] = 0
-    enable_epochs['BalanceWaitingListsEnableEpoch'] = 0
-    enable_epochs['SaveJailedAlwaysEnableEpoch'] = 0
-    enable_epochs['StakeEnableEpoch'] = 0
-    enable_epochs['StakingV2EnableEpoch'] = 1
-    enable_epochs['DoubleKeyProtectionEnableEpoch'] = 0
-    enable_epochs['ESDTEnableEpoch'] = 0
-    enable_epochs['GovernanceEnableEpoch'] = 1
-    enable_epochs['DelegationManagerEnableEpoch'] = 1
-    enable_epochs['DelegationSmartContractEnableEpoch'] = 1
-    enable_epochs['CorrectLastUnjailedEnableEpoch'] = 1
-    enable_epochs['RelayedTransactionsV2EnableEpoch'] = 2
-    enable_epochs['UnbondTokensV2EnableEpoch'] = 1
-    enable_epochs['SaveJailedAlwaysEnableEpoch'] = 1
-    enable_epochs['ReDelegateBelowMinCheckEnableEpoch'] = 1
-    enable_epochs['ValidatorToDelegationEnableEpoch'] = 1
-    enable_epochs['WaitingListFixEnableEpoch'] = 1000000
-    enable_epochs['IncrementSCRNonceInMultiTransferEnableEpoch'] = 0
-    enable_epochs['ESDTMultiTransferEnableEpoch'] = 0
-    enable_epochs['GlobalMintBurnDisableEpoch'] = 0
-    enable_epochs['ESDTTransferRoleEnableEpoch'] = 0
-    enable_epochs['BuiltInFunctionOnMetaEnableEpoch'] = 0
-    enable_epochs['ComputeRewardCheckpointEnableEpoch'] = 5
-    enable_epochs['BackwardCompSaveKeyValueEnableEpoch'] = 5
-    enable_epochs['SCRSizeInvariantCheckEnableEpoch'] = 5
-
-    enable_epochs['MultiESDTTransferFixOnCallBackOnEnableEpoch'] = 0
-    enable_epochs['ESDTNFTCreateOnMultiShard'] = 0
-    enable_epochs['RemoveNonUpdatedStorageEnableEpoch'] = 0
-    enable_epochs['FixOOGReturnCodeEnableEpoch'] = 0
-    enable_epochs['AddTokensToDelegationEnableEpoch'] = 0
-    enable_epochs['CorrectFirstQueuedEpoch'] = 0
-    enable_epochs['MetaESDTSetEnableEpoch'] = 0
-    enable_epochs['OptimizeGasUsedInCrossMiniBlocksEnableEpoch'] = 0
-    enable_epochs['DeleteDelegatorAfterClaimRewardsEnableEpoch'] = 0
-
-    enable_epochs['MaxNodesChangeEnableEpoch'] = [
-        {'EpochEnable': 0, 'MaxNumNodes': 36, 'NodesToShufflePerShard': 4},
-        {'EpochEnable': 1, 'MaxNumNodes': 56, 'NodesToShufflePerShard': 2}
-    ]
-
-    validate = multiversx_sdk_cli.config.get_value('testnet.validate_expected_keys') == 'true'
-
-    if validate:
-        validate_expected_keys(data['EnableEpochs'], enable_epochs)
-    data['EnableEpochs'].update(enable_epochs)
-
-    gas_schedule = dict()
-    gas_schedule['GasScheduleByEpochs'] = [
-        {'StartEpoch': 0, 'FileName': 'gasScheduleV5.toml'}
-    ]
-
-    if validate:
-        validate_expected_keys(data['GasSchedule'], gas_schedule)
-    data['GasSchedule'].update(gas_schedule)
-
-
-def patch_system_smart_contracts(data: ConfigDict, testnet_config: TestnetConfiguration):
-    data['StakingSystemSCConfig']['ActivateBLSPubKeyMessageVerification'] = True
-    data['ESDTSystemSCConfig']['EnabledEpoch'] = 0
-    data['GovernanceSystemSCConfig']['EnabledEpoch'] = 0
-    data['DelegationManagerSystemSCConfig']['EnabledEpoch'] = 1
-    data['DelegationSystemSCConfig']['EnabledEpoch'] = 1
-
-
-def validate_expected_keys(actual: ConfigDict, expected: ConfigDict):
-    actual_keys = set(actual.keys())
-    expected_keys = set(expected.keys())
-
-    unrecognized_keys = actual_keys - expected_keys
-    if len(unrecognized_keys) > 0:
-        raise ValueError(f'unrecognized configuration keys {unrecognized_keys}')
-
-    missing_keys = expected_keys - actual_keys
-    if len(missing_keys) > 0:
-        raise ValueError(f'missing configuration keys {missing_keys}')
diff --git a/multiversx_sdk_cli/testnet/setup.py b/multiversx_sdk_cli/testnet/setup.py
deleted file mode 100644
index 1de69989..00000000
--- a/multiversx_sdk_cli/testnet/setup.py
+++ /dev/null
@@ -1,281 +0,0 @@
-import logging
-import shutil
-from os import path
-from pathlib import Path
-from typing import Any
-
-import multiversx_sdk_cli.utils as utils
-from multiversx_sdk_cli import dependencies, myprocess, workstation
-from multiversx_sdk_cli.dependencies.install import install_module
-from multiversx_sdk_cli.testnet import (genesis_json,
-                                        genesis_smart_contracts_json,
-                                        node_config_toml, nodes_setup_json,
-                                        p2p_toml, wallets)
-from multiversx_sdk_cli.testnet.config import TestnetConfiguration
-
-logger = logging.getLogger("testnet")
-
-DEPENDENCY_KEYS = ["mx_chain_go", "mx_chain_proxy_go", "testwallets"]
-
-
-def install_dependencies():
-    dependencies.install_module("golang")
-
-    for key in DEPENDENCY_KEYS:
-        install_module(key, tag="", overwrite=True)
-
-
-def configure(args: Any):
-    testnet_config = TestnetConfiguration.from_file(args.configfile)
-    logger.info('testnet folder is %s', testnet_config.root())
-
-    create_folders(testnet_config)
-
-    # Validators and Observers
-    copy_config_to_nodes(testnet_config)
-    copy_validator_keys(testnet_config)
-    patch_node_config(testnet_config)
-
-    patch_nodes_p2p_config(
-        testnet_config,
-        testnet_config.validator_config_folders(),
-        testnet_config.networking['port_first_validator'],
-    )
-    patch_nodes_p2p_config(
-        testnet_config,
-        testnet_config.observer_config_folders(),
-        testnet_config.networking['port_first_observer'],
-    )
-    overwrite_nodes_setup(
-        testnet_config,
-        testnet_config.validator_config_folders(),
-    )
-    overwrite_nodes_setup(
-        testnet_config,
-        testnet_config.observer_config_folders(),
-    )
-    overwrite_genesis_file(
-        testnet_config,
-        testnet_config.validator_config_folders()
-    )
-    overwrite_genesis_file(
-        testnet_config,
-        testnet_config.observer_config_folders()
-    )
-
-    # Seed node
-    copy_config_to_seednode(testnet_config)
-    patch_seednode_p2p_config(testnet_config)
-    copy_seednode_p2p_key(testnet_config)
-
-    # Proxy
-    copy_config_to_proxy(testnet_config)
-    patch_proxy_config(testnet_config)
-
-    patch_source_code(testnet_config)
-    build_binaries(testnet_config)
-
-
-def clean(args):
-    testnet_config = TestnetConfiguration.from_file(args.configfile)
-    logger.info('testnet folder is %s', testnet_config.root())
-    utils.remove_folder(testnet_config.root())
-
-
-def create_folders(testnet_config: TestnetConfiguration):
-    makefolder(testnet_config.seednode_folder())
-
-    if testnet_config.features['proxy'] is True:
-        folder = testnet_config.proxy_folder()
-        makefolder(folder)
-        makefolder(folder / 'config')
-
-    for folder in testnet_config.all_nodes_folders():
-        makefolder(folder)
-
-
-def copy_config_to_nodes(testnet_config: TestnetConfiguration):
-    config_source = testnet_config.node_config_source()
-    for node_config in testnet_config.all_nodes_config_folders():
-        shutil.copytree(config_source, node_config)
-
-
-def copy_validator_keys(testnet_config: TestnetConfiguration):
-    for index, validator in enumerate(testnet_config.validators()):
-        shutil.copy(wallets.get_validator_key_file(index), validator.key_file_path())
-
-    # Currently, observers require validator PEM files as well
-    for index, observer in enumerate(testnet_config.observers()):
-        shutil.copy(wallets.get_observer_key_file(index), observer.key_file_path())
-
-
-def patch_node_config(testnet_config: TestnetConfiguration):
-    for node_config in testnet_config.all_nodes_config_folders():
-        node_config_file = node_config / 'config.toml'
-        data = utils.read_toml_file(node_config_file)
-        node_config_toml.patch_config(data, testnet_config)
-        utils.write_toml_file(node_config_file, data)
-
-        api_config_file = node_config / 'api.toml'
-        data = utils.read_toml_file(api_config_file)
-        node_config_toml.patch_api(data, testnet_config)
-        utils.write_toml_file(api_config_file, data)
-
-        system_sc_config_file = node_config / 'systemSmartContractsConfig.toml'
-        data = utils.read_toml_file(system_sc_config_file)
-        node_config_toml.patch_system_smart_contracts(data, testnet_config)
-        utils.write_toml_file(system_sc_config_file, data)
-
-        enable_epochs_config_file = node_config / 'enableEpochs.toml'
-        data = utils.read_toml_file(enable_epochs_config_file)
-        node_config_toml.patch_enable_epochs(data, testnet_config)
-        utils.write_toml_file(enable_epochs_config_file, data)
-
-        genesis_smart_contracts_file = node_config / 'genesisSmartContracts.json'
-        data = utils.read_json_file(genesis_smart_contracts_file)
-        genesis_smart_contracts_json.patch(data, testnet_config)
-        utils.write_json_file(genesis_smart_contracts_file, data)
-
-
-def copy_config_to_seednode(testnet_config: TestnetConfiguration):
-    config_source = testnet_config.node_source() / 'cmd' / 'seednode' / 'config'
-    seednode_config = testnet_config.seednode_config_folder()
-    makefolder(seednode_config)
-    shutil.copy(config_source / 'p2p.toml', seednode_config / 'p2p.toml')
-    shutil.copy(config_source / 'config.toml', seednode_config / 'config.toml')
-
-
-def patch_seednode_p2p_config(testnet_config: TestnetConfiguration):
-    seednode_config = testnet_config.seednode_config_folder()
-    seednode_config_file = seednode_config / 'p2p.toml'
-
-    data = utils.read_toml_file(seednode_config_file)
-    p2p_toml.patch_for_seednode(data, testnet_config)
-    utils.write_toml_file(seednode_config_file, data)
-
-
-def copy_seednode_p2p_key(testnet_config: TestnetConfiguration):
-    p2p_key_path = Path(__file__).parent / "seednode_p2pKey.pem"
-    shutil.copy(p2p_key_path, testnet_config.seednode_config_folder() / "p2pKey.pem")
-
-
-def patch_nodes_p2p_config(testnet_config: TestnetConfiguration, nodes_config_folders, port_first):
-    for index, config_folder in enumerate(nodes_config_folders):
-        config = config_folder / 'p2p.toml'
-        data = utils.read_toml_file(config)
-        p2p_toml.patch(data, testnet_config, index, port_first)
-        utils.write_toml_file(config, data)
-
-
-def overwrite_nodes_setup(testnet_config: TestnetConfiguration, nodes_config_folders):
-    nodes_setup = nodes_setup_json.build(testnet_config)
-
-    for index, config_folder in enumerate(nodes_config_folders):
-        config = config_folder / 'nodesSetup.json'
-        utils.write_json_file(str(config), nodes_setup)
-
-
-def overwrite_genesis_file(testnet_config: TestnetConfiguration, nodes_config_folders):
-    genesis = genesis_json.build(testnet_config)
-
-    for index, config_folder in enumerate(nodes_config_folders):
-        config = config_folder / 'genesis.json'
-        utils.write_json_file(str(config), genesis)
-
-
-def copy_config_to_proxy(testnet_config: TestnetConfiguration):
-    proxy_config_source = testnet_config.proxy_config_source()
-    proxy_config = testnet_config.proxy_config_folder()
-    makefolder(proxy_config)
-
-    shutil.copy(
-        proxy_config_source / 'config.toml',
-        proxy_config)
-
-    shutil.copytree(
-        proxy_config_source / 'apiConfig',
-        proxy_config / 'apiConfig')
-
-    shutil.copy(
-        proxy_config_source / 'external.toml',
-        proxy_config)
-
-
-def patch_proxy_config(testnet_config: TestnetConfiguration):
-    proxy_config_file = testnet_config.proxy_config_folder() / 'config.toml'
-    nodes = testnet_config.api_addresses_sharded_for_proxy_config()
-    data = utils.read_toml_file(proxy_config_file)
-    data['Observers'] = nodes
-    data['FullHistoryNodes'] = nodes
-    data['GeneralSettings']['ServerPort'] = testnet_config.proxy_port()
-    utils.write_toml_file(proxy_config_file, data)
-
-    api_config_file = path.join(testnet_config.proxy_config_folder(), 'apiConfig', 'v1_0.toml')
-    data = utils.read_toml_file(api_config_file)
-    routes = data['APIPackages']['transaction']['Routes']
-    for route in routes:
-        route["Open"] = True
-    utils.write_toml_file(api_config_file, data)
-
-
-def makefolder(path_where_to_make_folder):
-    path_where_to_make_folder.mkdir(parents=True, exist_ok=True)
-
-
-def patch_source_code(testnet_config: TestnetConfiguration):
-    logger.info("Patching the source code...")
-
-    node_source = testnet_config.node_source()
-
-    file = node_source / 'cmd' / 'node' / 'main.go'
-    content = utils.read_text_file(file)
-    content = content.replace("secondsToWaitForP2PBootstrap = 20", "secondsToWaitForP2PBootstrap = 1")
-    utils.write_file(file, content)
-
-
-def build_binaries(testnet_config: TestnetConfiguration):
-    golang = dependencies.get_golang()
-    golang_env = golang.get_env()
-    myprocess.run_process(['go', 'env'], env=golang_env)
-
-    logger.info("Building seednode...")
-    seednode_folder = testnet_config.node_source() / "cmd" / "seednode"
-    myprocess.run_process(['go', 'build'], cwd=seednode_folder, env=golang_env)
-
-    logger.info("Building node...")
-    node_folder = testnet_config.node_source() / "cmd" / "node"
-    myprocess.run_process(['go', 'build'], cwd=node_folder, env=golang_env)
-
-    wasm_vm_binary = testnet_config.wasm_vm_binary()
-    if wasm_vm_binary:
-        logger.warn("WASM VM does not require building anymore. Skipping...")
-
-    logger.info("Building proxy...")
-    proxy_folder = testnet_config.proxy_source() / "cmd" / "proxy"
-    myprocess.run_process(['go', 'build'], cwd=proxy_folder, env=golang_env)
-
-    # Now copy the binaries to the testnet folder
-    wasm_vm_package = _get_wasm_vm_package(testnet_config)
-    libwasmer_osx_path = Path(golang.get_gopath()) / "pkg" / "mod" / wasm_vm_package / "wasmer" / "libwasmer_darwin_amd64.dylib"
-
-    shutil.copy(seednode_folder / "seednode", testnet_config.seednode_folder())
-    if workstation.get_platform() == "osx":
-        shutil.copy(libwasmer_osx_path, testnet_config.seednode_folder())
-
-    for destination in testnet_config.all_nodes_folders():
-        shutil.copy(node_folder / "node", destination)
-
-        if workstation.get_platform() == "osx":
-            shutil.copy(libwasmer_osx_path, destination)
-
-    shutil.copy(proxy_folder / "proxy", testnet_config.proxy_folder())
-    if workstation.get_platform() == "osx":
-        shutil.copy(libwasmer_osx_path, testnet_config.proxy_folder())
-
-
-def _get_wasm_vm_package(testnet_config: TestnetConfiguration) -> str:
-    go_mod = testnet_config.node_source() / "go.mod"
-    lines = utils.read_lines(go_mod)
-    line = [line for line in lines if "github.com/multiversx/mx-chain-vm-v" in line][-1]
-    parts = line.split()
-    return f"{parts[0]}@{parts[1]}"
diff --git a/multiversx_sdk_cli/tests/shared.sh b/multiversx_sdk_cli/tests/shared.sh
index 9d27a409..57f3dda8 100755
--- a/multiversx_sdk_cli/tests/shared.sh
+++ b/multiversx_sdk_cli/tests/shared.sh
@@ -13,9 +13,10 @@ USERS=~/multiversx-sdk/testwallets/latest/users
 VALIDATORS=~/multiversx-sdk/testwallets/latest/validators
 DENOMINATION="000000000000000000"
 PROXY="${PROXY:-http://localhost:7950}"
-CHAIN_ID="${CHAIN_ID:-local-testnet}"
+CHAIN_ID="${CHAIN_ID:-localnet}"
 TestUser=./testdata/testUser.pem
 TestUser2=./testdata/testUser2.pem
+RUST_VERSION="nightly-2023-04-24"
 
 cleanSandbox() {
     rm -rf ${SANDBOX}
diff --git a/multiversx_sdk_cli/tests/test_cli_all.sh b/multiversx_sdk_cli/tests/test_cli_all.sh
index 3788c185..7592168d 100644
--- a/multiversx_sdk_cli/tests/test_cli_all.sh
+++ b/multiversx_sdk_cli/tests/test_cli_all.sh
@@ -7,7 +7,6 @@ testAll() {
     pushd $SCRIPT_DIR
     source ./shared.sh
     source ./test_cli_contracts.sh && testAll
-    source ./test_cli_wallet.sh && testAll
     source ./test_cli_dns.sh && testAll
 
     if [ -n "$USE_PROXY" ]; then
diff --git a/multiversx_sdk_cli/tests/test_cli_contracts.sh b/multiversx_sdk_cli/tests/test_cli_contracts.sh
index 5d8eaa23..d4ddc443 100755
--- a/multiversx_sdk_cli/tests/test_cli_contracts.sh
+++ b/multiversx_sdk_cli/tests/test_cli_contracts.sh
@@ -32,15 +32,15 @@ testBuildContracts() {
     assertFileExists ${SANDBOX}/myfactorial-rs/output/myfactorial-rs.wasm || return 1
     assertFileExists ${SANDBOX}/myfactorial-rs/output/myfactorial-rs.abi.json || return 1
 
-    ${CLI} contract build --ignore-eei-checks ${SANDBOX}/mybubbles-rs --cargo-target-dir=${TARGET_DIR} || return 1
+    ${CLI} contract build ${SANDBOX}/mybubbles-rs --cargo-target-dir=${TARGET_DIR} || return 1
     assertFileExists ${SANDBOX}/mybubbles-rs/output/mybubbles-rs.wasm || return 1
     assertFileExists ${SANDBOX}/mybubbles-rs/output/mybubbles-rs.abi.json || return 1
 
-    ${CLI} contract build --ignore-eei-checks ${SANDBOX}/mylottery-rs --cargo-target-dir=${TARGET_DIR} || return 1
+    ${CLI} contract build ${SANDBOX}/mylottery-rs --cargo-target-dir=${TARGET_DIR} || return 1
     assertFileExists ${SANDBOX}/mylottery-rs/output/mylottery-rs.wasm || return 1
     assertFileExists ${SANDBOX}/mylottery-rs/output/mylottery-rs.abi.json || return 1
 
-    ${CLI} contract build --ignore-eei-checks ${SANDBOX}/myfunding-rs --cargo-target-dir=${TARGET_DIR} || return 1
+    ${CLI} contract build ${SANDBOX}/myfunding-rs --cargo-target-dir=${TARGET_DIR} || return 1
     assertFileExists ${SANDBOX}/myfunding-rs/output/myfunding-rs.wasm || return 1
     assertFileExists ${SANDBOX}/myfunding-rs/output/myfunding-rs.abi.json || return 1
 }
@@ -122,7 +122,7 @@ testVerifyContract(){
     pkill -f local_verify_server.py
 }
 
-testReproducibleBuild(){
+testReproducibleBuild() {
     echo "testReproducibleBuild"
 
     wget -O ${SANDBOX}/example.zip https://github.com/multiversx/mx-reproducible-contract-build-example-sc/archive/refs/tags/v0.2.1.zip || return 1
@@ -132,6 +132,8 @@ testReproducibleBuild(){
 }
 
 testAll() {
+    ${CLI} config set dependencies.rust.tag ${RUST_VERSION}
+
     cleanSandbox || return 1
     testTrivialCommands || return 1
     testCreateContracts || return 1
diff --git a/multiversx_sdk_cli/tests/test_cli_deps.sh b/multiversx_sdk_cli/tests/test_cli_deps.sh
index 2f715465..e191201b 100644
--- a/multiversx_sdk_cli/tests/test_cli_deps.sh
+++ b/multiversx_sdk_cli/tests/test_cli_deps.sh
@@ -5,12 +5,10 @@ source "./shared.sh"
 testAll() {
     set -x
 
-    ${CLI} --verbose deps install rust
-    ${CLI} --verbose deps install clang
+    ${CLI} --verbose deps install rust --tag=${RUST_VERSION}
     ${CLI} --verbose deps install vmtools --overwrite
 
     ${CLI} --verbose deps check rust
-    ${CLI} --verbose deps check clang
     ${CLI} --verbose deps check vmtools
 
     set +x
diff --git a/multiversx_sdk_cli/tests/test_cli_localnet.sh b/multiversx_sdk_cli/tests/test_cli_localnet.sh
new file mode 100644
index 00000000..8e7cbcc7
--- /dev/null
+++ b/multiversx_sdk_cli/tests/test_cli_localnet.sh
@@ -0,0 +1,23 @@
+#!/usr/bin/env bash
+
+source "./shared.sh"
+
+testStartThenStop() {
+    pushd .
+
+    cleanSandbox
+    mkdir -p ${SANDBOX}/localnet_foo
+    cp ./testdata/localnet_with_resolution_remote.toml ${SANDBOX}/localnet_foo/localnet.toml
+    cd ${SANDBOX}/localnet_foo
+
+    ${CLI} localnet prerequisites
+    ${CLI} localnet build
+    ${CLI} localnet config
+
+    # Leave the localnet to run for some time, then assert for "time out error" (124).
+    timeout 1m ${CLI} localnet start || \
+    test $? -eq 124 || \
+    echo "Timeout error expected, but something else happened."
+
+    popd
+}
diff --git a/multiversx_sdk_cli/tests/test_cli_testnet.sh b/multiversx_sdk_cli/tests/test_cli_testnet.sh
deleted file mode 100644
index 112e9206..00000000
--- a/multiversx_sdk_cli/tests/test_cli_testnet.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env bash
-
-source "./shared.sh"
-
-testStart() {
-    pushd .
-
-    cleanSandbox
-    mkdir -p ${SANDBOX}/testnet_foo
-
-    ${CLI} config set dependencies.mx_chain_proxy_go.tag fix-node-ref
-    ${CLI} testnet prerequisites
-    
-    cp ./testdata/testnets/testnet_foo.toml ${SANDBOX}/testnet_foo/testnet.toml
-    cd ${SANDBOX}/testnet_foo
-    ${CLI} --verbose testnet config
-    ${CLI} --verbose testnet start
-
-    popd
-}
-
-testRestart() {
-    pushd .
-
-    cleanSandbox
-    mkdir -p ${SANDBOX}/testnet_foo
-
-    ${CLI} testnet prerequisites
-    
-    cp ./testdata/testnets/testnet_foo.toml ${SANDBOX}/testnet_foo/testnet.toml
-    cd ${SANDBOX}/testnet_foo
-    ${CLI} --verbose testnet config
-    ${CLI} --verbose testnet start
-    ${CLI} --verbose testnet start
-
-    popd
-}
diff --git a/multiversx_sdk_cli/tests/test_cli_wallet.py b/multiversx_sdk_cli/tests/test_cli_wallet.py
index f74a4137..86b29ba4 100644
--- a/multiversx_sdk_cli/tests/test_cli_wallet.py
+++ b/multiversx_sdk_cli/tests/test_cli_wallet.py
@@ -18,16 +18,6 @@ def test_wallet_new(capsys: Any):
 
 
 def test_wallet_new_and_save_in_pem_format(capsys: Any):
-    # Legacy method of invocation
-    outfile = testdata_out_path / "testWallet.pem"
-    outfile.unlink(missing_ok=True)
-    main(["wallet", "new", "--pem", "--output-path", str(outfile)])
-
-    expected_secret_key = Mnemonic(_read_stdout_mnemonic(capsys)).derive_key(0)
-    actual_secret_key = UserPEM.from_file(outfile).secret_key
-    assert actual_secret_key.hex() == expected_secret_key.hex()
-
-    # New method of invocation
     outfile = testdata_out_path / "testWallet.pem"
     outfile.unlink(missing_ok=True)
     main(["wallet", "new", "--format", "pem", "--outfile", str(outfile)])
@@ -38,18 +28,6 @@ def test_wallet_new_and_save_in_pem_format(capsys: Any):
 
 
 def test_wallet_new_and_save_in_json_format(capsys: Any, monkeypatch: Any):
-    # Legacy method of invocation
-    outfile = testdata_out_path / "testWallet.json"
-    outfile.unlink(missing_ok=True)
-    _mock_getpass(monkeypatch, "password")
-    main(["wallet", "new", "--json", "--output-path", str(outfile)])
-
-    expected_mnemonic = Mnemonic(_read_stdout_mnemonic(capsys))
-    keyfile = json.loads(outfile.read_text())
-    actual_mnemonic = UserWallet.decrypt_mnemonic(keyfile, "password")
-    assert actual_mnemonic.get_text() == expected_mnemonic.get_text()
-
-    # New method of invocation
     outfile = testdata_out_path / "testWallet.json"
     outfile.unlink(missing_ok=True)
     _mock_getpass(monkeypatch, "password")
@@ -61,18 +39,6 @@ def test_wallet_new_and_save_in_json_format(capsys: Any, monkeypatch: Any):
     assert actual_mnemonic.get_text() == expected_mnemonic.get_text()
 
 
-def test_wallet_derive_pem_from_mnemonic_deprecated(monkeypatch: Any):
-    outfile = testdata_out_path / "wallet.pem"
-    test_mnemonic = "moral volcano peasant pass circle pen over picture flat shop clap goat never lyrics gather prepare woman film husband gravity behind test tiger improve"
-    _mock_stdin(monkeypatch, test_mnemonic)
-
-    main(["wallet", "derive", str(outfile), "--mnemonic"])
-
-    expected_secret_key = Mnemonic(test_mnemonic).derive_key(0)
-    actual_secret_key = UserPEM.from_file(outfile).secret_key
-    assert actual_secret_key.hex() == expected_secret_key.hex()
-
-
 def test_wallet_new_as_mnemonic():
     outfile = testdata_out_path / "wallet.txt"
     outfile.unlink(missing_ok=True)
@@ -233,13 +199,31 @@ def test_wallet_convert_keystore_with_secret_key_to_pem(monkeypatch: Any):
     assert pem.secret_key.hex() == "413f42575f7f26fad3317a778771212fdb80245850981e48b58a4f25e344e8f9"
 
 
+def test_wallet_bech32_encode(capsys: Any):
+    main([
+        "wallet", "bech32", "--encode", "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"
+    ])
+
+    out = _read_stdout(capsys)
+    assert out == "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"
+
+
+def test_wallet_bech32_decode(capsys: Any):
+    main([
+        "wallet", "bech32", "--decode", "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th"
+    ])
+
+    out = _read_stdout(capsys)
+    assert out == "0139472eff6886771a982f3083da5d421f24c29181e63888228dc81ca60d69e1"
+
+
 def _read_stdout_mnemonic(capsys: Any) -> str:
-    return capsys.readouterr().out.replace("Mnemonic:", "").strip()
+    return _read_stdout(capsys).replace("Mnemonic:", "").strip()
 
 
-def _mock_getpass(monkeypatch: Any, password: str):
-    monkeypatch.setattr(getpass, "getpass", lambda _: password)
+def _read_stdout(capsys: Any) -> str:
+    return capsys.readouterr().out.strip()
 
 
-def _mock_stdin(monkeypatch: Any, text: str):
-    monkeypatch.setattr("builtins.input", lambda _: text)
+def _mock_getpass(monkeypatch: Any, password: str):
+    monkeypatch.setattr(getpass, "getpass", lambda _: password)
diff --git a/multiversx_sdk_cli/tests/test_cli_wallet.sh b/multiversx_sdk_cli/tests/test_cli_wallet.sh
deleted file mode 100644
index 2387a65d..00000000
--- a/multiversx_sdk_cli/tests/test_cli_wallet.sh
+++ /dev/null
@@ -1,36 +0,0 @@
-#!/usr/bin/env bash
-
-source "./shared.sh"
-
-testAll() {
-    testDerive || return 1
-    testDeriveWithSpaces || return 1
-    testNewPem || return 1
-    testBech32 || return 1
-}
-
-testDerive() {
-    echo "testDerive"
-    ${CLI} wallet derive ./testdata-out/myaccount.pem || return 1
-    assertFileExists "./testdata-out/myaccount.pem" || return 1
-    echo -e "moral volcano peasant pass circle pen over picture flat shop clap goat never lyrics gather prepare woman film husband gravity behind test tiger improve\n" | ${CLI} wallet derive --mnemonic ./testdata-out/myaccount-from-mnemonic.pem || return 1
-    assertFileExists "./testdata-out/myaccount-from-mnemonic.pem" || return 1
-}
-
-testDeriveWithSpaces() {
-    echo "testDeriveWithSpaces"
-    echo -e "moral volcano peasant pass circle pen over picture flat shop clap goat never lyrics gather prepare woman film husband gravity behind test tiger improve     " | ${CLI} wallet derive --mnemonic ./testdata-out/myaccount-from-mnemonic.pem || return 1
-    assertFileExists "./testdata-out/myaccount-from-mnemonic.pem" || return 1
-}
-
-testNewPem() {
-    echo "testNewPem"
-    ${CLI} wallet new --pem --output-path=./testdata-out/newWallet.pem || return 1
-    assertFileExists "./testdata-out/newWallet.pem" || return 1
-}
-
-testBech32() {
-    echo "testBech32"
-    ${CLI} wallet bech32 --encode 000000000000000005006e4f90488e27342f9a46e1809452c85ee7186566bd5e || return 1
-    ${CLI} wallet bech32 --decode erd1qqqqqqqqqqqqqpgqde8eqjywyu6zlxjxuxqfg5kgtmn3setxh40qen8egy || return 1
-}
diff --git a/multiversx_sdk_cli/tests/test_proxy.py b/multiversx_sdk_cli/tests/test_proxy.py
index a1830dc9..8696b445 100644
--- a/multiversx_sdk_cli/tests/test_proxy.py
+++ b/multiversx_sdk_cli/tests/test_proxy.py
@@ -1,23 +1,8 @@
-from pathlib import Path
-from multiversx_sdk_cli.cli import main
-from multiversx_sdk_cli.accounts import Account
-from multiversx_sdk_network_providers.proxy_network_provider import ProxyNetworkProvider
-
+from multiversx_sdk_network_providers.proxy_network_provider import \
+    ProxyNetworkProvider
 
-def test_get_transactions():
-    output_file = Path(__file__).parent / "testdata-out" / "transactions.txt"
-
-    main(
-        [
-            "account",
-            "get-transactions",
-            "--address",
-            "erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th",
-            "--outfile",
-            str(output_file),
-        ]
-    )
-    assert Path.is_file(output_file) == True
+from multiversx_sdk_cli.accounts import Account
+from multiversx_sdk_cli.cli import main
 
 
 def test_get_account():
diff --git a/multiversx_sdk_cli/tests/test_testnet.py b/multiversx_sdk_cli/tests/test_testnet.py
index e1c99eb2..40c999fe 100644
--- a/multiversx_sdk_cli/tests/test_testnet.py
+++ b/multiversx_sdk_cli/tests/test_testnet.py
@@ -1,73 +1,48 @@
 import os
 import sys
-from pathlib import Path
+from typing import Any, Dict
 
-import multiversx_sdk_cli.config
-from multiversx_sdk_cli import workstation
-from multiversx_sdk_cli.testnet import config
+from multiversx_sdk_cli.localnet.config_root import ConfigRoot
+from multiversx_sdk_cli.localnet.config_software import SoftwareResolution
 
-sys.path = [os.getcwd() + '/..'] + sys.path
+sys.path = [os.getcwd() + "/.."] + sys.path
 
 
-def test_merge_configs():
-    left = dict()
-    left['networking'] = {
-        'port_proxy': 7950,
-        'port_seednode_port': 9999,
-        'somekey': 'somestring',
-    }
-    left['metashard'] = {
-        'metashardID': 4294967295,
-        'observers': 0,
-        'validators': 1,
-    }
+def test_override_config():
+    config = ConfigRoot()
 
-    right = dict()
-    right['metashard'] = {
-        'consensus_size': 1,
-        'metashardID': 4294967295,
-        'validators': 4,
-    }
-    right['timing'] = {
-        'genesis_delay': 30,
-    }
+    # Check a few default values
+    assert config.general.rounds_per_epoch == 100
+    assert config.general.round_duration_milliseconds == 6000
+    assert config.metashard.consensus_size == 1
+    assert config.networking.port_proxy == 7950
+    assert config.software.mx_chain_go.resolution == SoftwareResolution.Remote
+    assert config.software.mx_chain_go.archive_url == "https://github.com/multiversx/mx-chain-go/archive/refs/heads/master.zip"
 
-    expected_merged = dict()
-    expected_merged['metashard'] = {
-        'consensus_size': 1,
-        'metashardID': 4294967295,
-        'observers': 0,
-        'validators': 4,
+    # Now partly override the config
+    config_patch: Dict[str, Any] = dict()
+    config_patch["general"] = {
+        "rounds_per_epoch": 200,
+        "round_duration_milliseconds": 4000,
     }
-    expected_merged['timing'] = {
-        'genesis_delay': 30
+    config_patch["metashard"] = {
+        "consensus_size": 2,
     }
-    expected_merged['networking'] = {
-        'port_proxy': 7950,
-        'port_seednode_port': 9999,
-        'somekey': 'somestring',
+    config_patch["networking"] = {
+        "port_proxy": 7951,
     }
-
-    result_merged = config.merge_configs(left, right)
-    assert expected_merged == result_merged
-
-
-def test_init():
-    data = dict()
-    data['folders'] = {
-        'mx_chain_go': '{MULTIVERSX_SDK}/bar',
-        'mx_chain_proxy_go': '{MULTIVERSX_SDK}/foobar',
-        'testnet': '/some/where/mytestnet',
+    config_patch["software"] = {
+        "mx_chain_go": {
+            "archive_url": "https://github.com/multiversx/mx-chain-go/archive/refs/tags/v1.5.1.zip"
+        }
     }
 
-    sdk_folder = workstation.get_tools_folder()
-    node_folder = multiversx_sdk_cli.config.get_dependency_parent_directory('mx_chain_go')
-    (node_folder / 'v1.2.3').mkdir(parents=True, exist_ok=True)
-
-    proxy_folder = multiversx_sdk_cli.config.get_dependency_parent_directory('mx_chain_proxy_go')
-    (proxy_folder / 'v2.3.4').mkdir(parents=True, exist_ok=True)
+    config.override(config_patch)
 
-    testnet_config = config.TestnetConfiguration(data)
-    assert testnet_config.folders["mx_chain_go"] == sdk_folder / "bar"
-    assert testnet_config.folders["mx_chain_proxy_go"] == sdk_folder / "foobar"
-    assert testnet_config.folders["testnet"] == Path("/some/where/mytestnet")
+    # Check the overridden values
+    assert config.general.rounds_per_epoch == 200
+    assert config.general.round_duration_milliseconds == 4000
+    assert config.metashard.consensus_size == 2
+    assert config.networking.port_proxy == 7951
+    assert config.software.mx_chain_go.resolution == SoftwareResolution.Remote
+    assert config.software.mx_chain_go.archive_url == "https://github.com/multiversx/mx-chain-go/archive/refs/tags/v1.5.1.zip"
diff --git a/multiversx_sdk_cli/tests/test_transactions.py b/multiversx_sdk_cli/tests/test_transactions.py
index f123e418..7396ba0c 100644
--- a/multiversx_sdk_cli/tests/test_transactions.py
+++ b/multiversx_sdk_cli/tests/test_transactions.py
@@ -58,14 +58,14 @@ def test_serialize_transaction_with_usernames(self):
         transaction.gasPrice = 1000000000
         transaction.gasLimit = 50000
         transaction.data = ""
-        transaction.chainID = "local-testnet"
+        transaction.chainID = "localnet"
         transaction.version = 1
 
         serialized = transaction.serialize().decode()
-        self.assertEqual("""{"nonce":89,"value":"0","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","senderUsername":"YWxpY2U=","receiverUsername":"Ym9i","gasPrice":1000000000,"gasLimit":50000,"chainID":"local-testnet","version":1}""", serialized)
+        self.assertEqual("""{"nonce":89,"value":"0","receiver":"erd1spyavw0956vq68xj8y4tenjpq2wd5a9p2c6j8gsz7ztyrnpxrruqzu66jx","sender":"erd1qyu5wthldzr8wx5c9ucg8kjagg0jfs53s8nr3zpz3hypefsdd8ssycr6th","senderUsername":"YWxpY2U=","receiverUsername":"Ym9i","gasPrice":1000000000,"gasLimit":50000,"chainID":"localnet","version":1}""", serialized)
 
         transaction.sign(self.alice)
-        self.assertEqual("1bed82c3f91c9d24f3a163e7b93a47453d70e8743201fe7d3656c0214569566a76503ef0968279ac942ca43b9c930bd26638dfb075a220ce80b058ab7bca140a", transaction.signature)
+        self.assertEqual("f872ea6d43b86ba6b22a7f62319a972124fc6dbd6b0743fa6465819a14d7a90100be865affb030bddf7ed86e648236cd20b1d5078e805b5419d43f3c5032b006", transaction.signature)
 
     def test_serialize_transaction_as_inner(self):
         transaction = Transaction()
diff --git a/multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml b/multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml
new file mode 100644
index 00000000..aa2284af
--- /dev/null
+++ b/multiversx_sdk_cli/tests/testdata/localnet_with_resolution_local.toml
@@ -0,0 +1,12 @@
+[general]
+log_level = "*:DEBUG"
+rounds_per_epoch = 40
+round_duration_milliseconds = 3000
+
+[software.mx_chain_go]
+resolution = "local"
+local_path = "~/multiversx-sdk/sandbox/mx-chain-go"
+
+[software.mx_chain_proxy_go]
+resolution = "local"
+local_path = "~/multiversx-sdk/sandbox/mx-chain-proxy-go"
diff --git a/multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml b/multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml
new file mode 100644
index 00000000..bbaa7975
--- /dev/null
+++ b/multiversx_sdk_cli/tests/testdata/localnet_with_resolution_remote.toml
@@ -0,0 +1,12 @@
+[general]
+log_level = "*:DEBUG"
+rounds_per_epoch = 40
+round_duration_milliseconds = 3000
+
+[software.mx_chain_go]
+resolution = "remote"
+archive_url = "https://github.com/multiversx/mx-chain-go/archive/refs/heads/master.zip"
+
+[software.mx_chain_proxy_go]
+resolution = "remote"
+archive_url = "https://github.com/multiversx/mx-chain-proxy-go/archive/refs/heads/master.zip"
diff --git a/multiversx_sdk_cli/tests/testdata/testnets/testnet_foo.toml b/multiversx_sdk_cli/tests/testdata/testnets/testnet_foo.toml
deleted file mode 100644
index 12acb429..00000000
--- a/multiversx_sdk_cli/tests/testdata/testnets/testnet_foo.toml
+++ /dev/null
@@ -1,29 +0,0 @@
-[features]
-loglevel = "*:INFO"
-proxy = true
-
-[metashard]
-consensus_size = 1
-metashardID = 4294967295
-observers = 1
-validators = 1
-
-[networking]
-host = "127.0.0.1"
-port_seednode = 9999
-p2p_id_seednode = "16Uiu2HAkx4QqgXXDdHdUWbLu5kxhd3Uo2hqB2FfCxmxH5Sd7bZFk"
-port_proxy = 7950
-port_first_observer = 21100
-port_first_observer_rest_api = 10000
-port_first_validator = 21500
-port_first_validator_rest_api = 10100
-
-[shards]
-consensus_size = 1
-count = 1
-observers_per_shard = 1
-validators_per_shard = 1
-
-[timing]
-genesis_delay = 30
-