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

Issue:
Testnet and typing fixes
This PR updates `erdpy testnet` to work with newer `elrond-go` versions. This mostly means support for the `enableEpochs.toml` configuration file.

And many typing fixes.

TODO: convert all tests from `unittest` to the much simpler `pytest`.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index a3b506f8..c7bb3f97 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,3 +2,13 @@
 dist
 __pycache__
 *.egg-info
+*.py[cod]
+*$py.class
+.pytest_cache/
+erdpy/tests/testdata-out
+
+# mypy
+.mypy_cache/
+
+# Tags generated by ctags
+tags
diff --git a/Makefile b/Makefile
index 699cdd87..1f2c3ee1 100644
--- a/Makefile
+++ b/Makefile
@@ -6,3 +6,8 @@ build-erdpy: clean
 
 publish-erdpy: build-erdpy
 	twine upload dist/*
+
+test:
+	python3 -m unittest discover -s erdpy/tests
+	pytest ./erdpy/tests/test_testnet.py -s
+	source ./erdpy/tests/test_cli_all.sh && testAll || return 1
diff --git a/README.md b/README.md
index 61c67004..7b9c12fb 100644
--- a/README.md
+++ b/README.md
@@ -4,13 +4,11 @@ Elrond Python Command Line Tools and SDK for interacting with the Elrond Network
 ## Documentation
 [docs.elrond.com](https://docs.elrond.com/sdk-and-tools/erdpy/erdpy/)
 
-[pkg.go.dev](https://pkg.go.dev/github.com/ElrondNetwork/elrond-sdk/erdgo)
-
 ## CLI
-[CLI](CLI.md)
+[CLI](erdpy/CLI.md)
 
 ## Distribution
 [erdpy-up](https://docs.elrond.com/sdk-and-tools/erdpy/installing-erdpy/) and [PyPi](https://pypi.org/project/erdpy/#history)
 
 ## CHANGELOG
-[CHANGELOG](CHANGELOG.md)
+[CHANGELOG](erdpy/CHANGELOG.md)
diff --git a/erdpy/CLI.md b/erdpy/CLI.md
index c7969536..44f2ba72 100644
--- a/erdpy/CLI.md
+++ b/erdpy/CLI.md
@@ -46,6 +46,7 @@ testnet                        Set up, start and control local testnets
 data                           Data manipulation omnitool
 staking-provider               Staking provider omnitool
 dns                            Operations related to the Domain Name Service
+ledger                         Get Ledger App address and version
 
 ```
 ## Group **Contract**
@@ -159,10 +160,13 @@ optional arguments:
   --metadata-not-upgradeable             ‼ mark the contract as NOT upgradeable (default: upgradeable)
   --metadata-payable                     ‼ mark the contract as payable (default: not payable)
   --outfile OUTFILE                      where to save the output (default: stdout)
-  --pem PEM                              🔑 the PEM file, if keyfile not provided
+  --pem PEM                              🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX                  🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                    🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                               🔐 bool flag for signing transaction using ledger
+  --ledger-account-index                 🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index                 🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME      🖄 the username of the sender
   --proxy PROXY                          🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
   --nonce NONCE                          # the nonce for the transaction
@@ -193,10 +197,13 @@ positional arguments:
 optional arguments:
   -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
+  --pem PEM                              🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX                  🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                    🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                               🔐 bool flag for signing transaction using ledger
+  --ledger-account-index                 🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index                 🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME      🖄 the username of the sender
   --proxy PROXY                          🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
   --nonce NONCE                          # the nonce for the transaction
@@ -233,10 +240,13 @@ optional arguments:
   --bytecode BYTECODE                    the WASM file
   --metadata-not-upgradeable             ‼ mark the contract as NOT upgradeable (default: upgradeable)
   --metadata-payable                     ‼ mark the contract as payable (default: not payable)
-  --pem PEM                              🔑 the PEM file, if keyfile not provided
+  --pem PEM                              🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX                  🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                    🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                               🔐 bool flag for signing transaction using ledger
+  --ledger-account-index                 🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index                 🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME      🖄 the username of the sender
   --proxy PROXY                          🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
   --nonce NONCE                          # the nonce for the transaction
@@ -306,10 +316,13 @@ Create a new transaction
 
 optional arguments:
   -h, --help                             show this help message and exit
-  --pem PEM                              🔑 the PEM file, if keyfile not provided
+  --pem PEM                              🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX                  🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                    🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                               🔐 bool flag for signing transaction using ledger
+  --ledger-account-index                 🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index                 🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME      🖄 the username of the sender
   --nonce NONCE                          # the nonce for the transaction
   --recall-nonce                         ⭮ whether to recall the nonce when creating the transaction (default: False)
@@ -432,10 +445,13 @@ Stake value into the Network
 optional arguments:
   -h, --help                         show this help message and exit
   --proxy PROXY                      🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
-  --pem PEM                          🔑 the PEM file, if keyfile not provided
+  --pem PEM                          🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX              🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                           🔐 bool flag for signing transaction using ledger
+  --ledger-account-index             🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index             🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME  🖄 the username of the sender
   --nonce NONCE                      # the nonce for the transaction
   --recall-nonce                     ⭮ whether to recall the nonce when creating the transaction (default: False)
@@ -464,10 +480,13 @@ Unstake value
 optional arguments:
   -h, --help                             show this help message and exit
   --proxy PROXY                          🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
-  --pem PEM                              🔑 the PEM file, if keyfile not provided
+  --pem PEM                              🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX                  🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                    🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                               🔐 bool flag for signing transaction using ledger
+  --ledger-account-index                 🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index                 🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME      🖄 the username of the sender
   --nonce NONCE                          # the nonce for the transaction
   --recall-nonce                         ⭮ whether to recall the nonce when creating the transaction (default: False)
@@ -495,10 +514,13 @@ Unjail a Validator Node
 optional arguments:
   -h, --help                             show this help message and exit
   --proxy PROXY                          🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
-  --pem PEM                              🔑 the PEM file, if keyfile not provided
+  --pem PEM                              🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX                  🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                    🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                               🔐 bool flag for signing transaction using ledger
+  --ledger-account-index                 🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index                 🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME      🖄 the username of the sender
   --nonce NONCE                          # the nonce for the transaction
   --recall-nonce                         ⭮ whether to recall the nonce when creating the transaction (default: False)
@@ -526,10 +548,13 @@ Unbond
 optional arguments:
   -h, --help                             show this help message and exit
   --proxy PROXY                          🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
-  --pem PEM                              🔑 the PEM file, if keyfile not provided
+  --pem PEM                              🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX                  🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                      🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                    🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                               🔐 bool flag for signing transaction using ledger
+  --ledger-account-index                 🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index                 🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME      🖄 the username of the sender
   --nonce NONCE                          # the nonce for the transaction
   --recall-nonce                         ⭮ whether to recall the nonce when creating the transaction (default: False)
@@ -557,10 +582,13 @@ Change the reward address
 optional arguments:
   -h, --help                         show this help message and exit
   --proxy PROXY                      🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
-  --pem PEM                          🔑 the PEM file, if keyfile not provided
+  --pem PEM                          🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX              🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                           🔐 bool flag for signing transaction using ledger
+  --ledger-account-index             🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index             🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME  🖄 the username of the sender
   --nonce NONCE                      # the nonce for the transaction
   --recall-nonce                     ⭮ whether to recall the nonce when creating the transaction (default: False)
@@ -588,10 +616,13 @@ Claim rewards
 optional arguments:
   -h, --help                         show this help message and exit
   --proxy PROXY                      🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
-  --pem PEM                          🔑 the PEM file, if keyfile not provided
+  --pem PEM                          🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX              🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                           🔐 bool flag for signing transaction using ledger
+  --ledger-account-index             🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index             🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME  🖄 the username of the sender
   --nonce NONCE                      # the nonce for the transaction
   --recall-nonce                     ⭮ whether to recall the nonce when creating the transaction (default: False)
@@ -1009,10 +1040,13 @@ Dispatch queued transactions
 optional arguments:
   -h, --help                         show this help message and exit
   --proxy PROXY                      🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
-  --pem PEM                          🔑 the PEM file, if keyfile not provided
+  --pem PEM                          🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX              🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                           🔐 bool flag for signing transaction using ledger
+  --ledger-account-index             🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index             🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME  🖄 the username of the sender
 
 ```
@@ -1028,10 +1062,13 @@ Continuously dispatch queued transactions
 optional arguments:
   -h, --help                         show this help message and exit
   --proxy PROXY                      🖧 the URL of the proxy (default: https://testnet-gateway.elrond.com)
-  --pem PEM                          🔑 the PEM file, if keyfile not provided
+  --pem PEM                          🔑 the PEM file, if keyfile or ledger are not provided
   --pem-index PEM_INDEX              🔑 the index in the PEM file (default: 0)
-  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM not provided
+  --keyfile KEYFILE                  🔑 a JSON keyfile, if PEM of ledger not provided
   --passfile PASSFILE                🔑 a file containing keyfile's password, if keyfile provided
+  --ledger                           🔐 bool flag for signing transaction using ledger
+  --ledger-account-index             🔐 the index of the account (only applicable if --ledger is set)
+  --ledger-address-index             🔐 the index of the address (only applicable if --ledger is set)
   --sender-username SENDER_USERNAME  🖄 the username of the sender
   --interval INTERVAL                the interval to retrieve transactions from the queue, in seconds
 
@@ -1364,3 +1401,46 @@ optional arguments:
   --use-global           use the global storage (default: False)
 
 ```
+
+## Group **Ledger**
+
+
+
+```
+usage: erdpy ledger COMMAND [-h] ...
+
+Get Ledger App addresses and version
+
+COMMANDS:
+  {addresses,version}
+
+OPTIONS:
+  -h, --help           show this help message and exit
+
+----------------
+COMMANDS summary
+----------------
+addresses                      Print multiple addresses for the Ledger device
+version                        Print the Elrond App version
+```
+
+### Ledger.addresses
+```
+usage: erdpy ledger addresses [-h] ...
+
+Get the addresses within Ledger
+
+optional arguments:
+  -h, --help                     show this help message and exit
+  --num-addresses NUM_ADDRESSES  The number of addresses to fetch
+```
+
+### Ledger.version
+```
+usage: erdpy ledger version [-h] ...
+
+Get the version of the Elrond App for Ledger
+
+optional arguments:
+  -h, --help  show this help message and exit
+```
diff --git a/erdpy/accounts.py b/erdpy/accounts.py
index 8b127c14..9538dd38 100644
--- a/erdpy/accounts.py
+++ b/erdpy/accounts.py
@@ -2,9 +2,10 @@
 import os
 from binascii import unhexlify
 from os import path
-from typing import Any, Union
+from typing import Any, Optional
 
 from erdpy import constants, errors, utils
+from erdpy.errors import LedgerError
 from erdpy.interfaces import IAccount, IAddress
 from erdpy.wallet import bech32, generate_pair, pem
 from erdpy.wallet.keyfile import get_password, load_from_key_file
@@ -44,13 +45,20 @@ def get_all(self):
 
 
 class Account(IAccount):
-    def __init__(self, address: Any = None, pem_file: Union[str, None] = None, pem_index: int = 0, key_file: str = "", pass_file: str = ""):
+    def __init__(self,
+                 address: Any = None,
+                 pem_file: Optional[str] = None,
+                 pem_index: int = 0,
+                 key_file: str = "",
+                 pass_file: str = "",
+                 ledger: bool = False):
         self.address = Address(address)
         self.pem_file = pem_file
         self.pem_index = int(pem_index)
         self.nonce: int = 0
+        self.ledger = ledger
 
-        if pem_file:
+        if self.pem_file:
             seed, pubkey = pem.parse(self.pem_file, self.pem_index)
             self.private_key_seed = seed.hex()
             self.address = Address(pubkey)
@@ -66,6 +74,8 @@ def sync_nonce(self, proxy: Any):
         logger.info(f"Account.sync_nonce() done: {self.nonce}")
 
     def get_seed(self) -> bytes:
+        if self.ledger:
+            raise LedgerError("cannot get seed from a Ledger account")
         return unhexlify(self.private_key_seed)
 
 
@@ -74,9 +84,10 @@ class Address(IAddress):
     PUBKEY_LENGTH = 32
     PUBKEY_STRING_LENGTH = PUBKEY_LENGTH * 2    # hex-encoded
     BECH32_LENGTH = 62
+    _value_hex: str
 
     def __init__(self, value):
-        self._value_hex = None
+        self._value_hex = ''
 
         if not value:
             return
@@ -102,7 +113,9 @@ def hex(self) -> str:
     def bech32(self) -> str:
         self._assert_validity()
         pubkey = self.pubkey()
-        return bech32.bech32_encode(self.HRP, bech32.convertbits(pubkey, 8, 5))
+        b32 = bech32.bech32_encode(self.HRP, bech32.convertbits(pubkey, 8, 5))
+        assert isinstance(b32, str)
+        return b32
 
     def pubkey(self):
         self._assert_validity()
@@ -113,7 +126,7 @@ def is_contract_address(self):
         return self.hex().startswith(constants.SC_HEX_PUBKEY_PREFIX)
 
     def _assert_validity(self):
-        if self._value_hex is None:
+        if self._value_hex == '':
             raise errors.EmptyAddressError()
 
     def __repr__(self):
diff --git a/erdpy/cli.py b/erdpy/cli.py
index ba287c6d..0ceb37d2 100644
--- a/erdpy/cli.py
+++ b/erdpy/cli.py
@@ -13,6 +13,7 @@
 import erdpy.cli_data
 import erdpy.cli_deps
 import erdpy.cli_dispatcher
+import erdpy.cli_ledger
 import erdpy.cli_network
 import erdpy.cli_testnet
 import erdpy.cli_transactions
@@ -85,6 +86,7 @@ def setup_parser():
     commands.append(erdpy.cli_transactions.setup_parser(subparsers))
     commands.append(erdpy.cli_validators.setup_parser(subparsers))
     commands.append(erdpy.cli_accounts.setup_parser(subparsers))
+    commands.append(erdpy.cli_ledger.setup_parser(subparsers))
     commands.append(erdpy.cli_wallet.setup_parser(subparsers))
     commands.append(erdpy.cli_network.setup_parser(subparsers))
     commands.append(erdpy.cli_cost.setup_parser(subparsers))
diff --git a/erdpy/cli_contracts.py b/erdpy/cli_contracts.py
index 55ae7020..5744bd4a 100644
--- a/erdpy/cli_contracts.py
+++ b/erdpy/cli_contracts.py
@@ -2,6 +2,8 @@
 import os
 from typing import Any
 
+from pathlib import Path
+
 from erdpy import cli_shared, errors, projects, utils
 from erdpy.accounts import Account, Address
 from erdpy.contracts import CodeMetadata, SmartContract
@@ -110,8 +112,10 @@ def _add_project_arg(sub: Any):
 
 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", help="the WASM file")
+    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_contract_arg(sub: Any):
@@ -217,8 +221,8 @@ def dump_tx_and_result(tx: Any, result: Any, args: Any):
 
 
 def _prepare_contract(args: Any) -> SmartContract:
-    if args.bytecode:
-        bytecode = utils.read_file(args.bytecode, binary=True).hex()
+    if len(args.bytecode):
+        bytecode = utils.read_binary_file(Path(args.bytecode)).hex()
     else:
         project = load_project(args.project)
         bytecode = project.get_bytecode()
diff --git a/erdpy/cli_ledger.py b/erdpy/cli_ledger.py
new file mode 100644
index 00000000..fd40a503
--- /dev/null
+++ b/erdpy/cli_ledger.py
@@ -0,0 +1,34 @@
+from erdpy.ledger.ledger_app_handler import ElrondLedgerApp
+from erdpy import cli_shared
+import logging
+from typing import Any
+
+logger = logging.getLogger("cli.ledger")
+
+
+def setup_parser(subparsers: Any) -> Any:
+    parser = cli_shared.add_group_subparser(subparsers, "ledger", "Get Ledger App addresses and version")
+    subparsers = parser.add_subparsers()
+
+    sub = cli_shared.add_command_subparser(subparsers, "ledger", "addresses", "Get the addresses within Ledger")
+    sub.add_argument("--num-addresses", required=False, type=int, default=10, help="The number of addresses to fetch")
+    sub.set_defaults(func=print_addresses)
+
+    sub = cli_shared.add_command_subparser(subparsers, "ledger", "version", "Get the version of the Elrond App for Ledger")
+    sub.set_defaults(func=print_version)
+
+    return subparsers
+
+
+def print_addresses(args):
+    ledger_app = ElrondLedgerApp()
+    for i in range(args.num_addresses):
+        address = ledger_app.get_address(0, i)
+        print('account index = %d | address index = %d | address: %s' % (0, i, address))
+    ledger_app.close()
+
+
+def print_version(args):
+    ledger_app = ElrondLedgerApp()
+    print("Elrond App version: " + ledger_app.get_version())
+    ledger_app.close()
diff --git a/erdpy/cli_shared.py b/erdpy/cli_shared.py
index be27cf9f..f268b58f 100644
--- a/erdpy/cli_shared.py
+++ b/erdpy/cli_shared.py
@@ -1,11 +1,13 @@
 import argparse
 import ast
+import string
 import sys
 from argparse import FileType
 from typing import Any, List, Text
 
 from erdpy import config, errors, scope, utils
 from erdpy.accounts import Account
+from erdpy.ledger.ledger_functions import do_get_ledger_address
 from erdpy.proxy.core import ElrondProxy
 from erdpy.transactions import Transaction
 
@@ -69,18 +71,22 @@ def add_tx_args(sub: Any, with_nonce: bool = True, with_receiver: bool = True, w
 
     sub.add_argument("--chain", default=scope.get_chain_id(), help="the chain identifier (default: %(default)s)")
     sub.add_argument("--version", type=int, default=scope.get_tx_version(), help="the transaction version (default: %(default)s)")
+    sub.add_argument("--options", type=int, default=0,  help="the transaction options (default: 0)")
 
 
 def add_wallet_args(sub: Any):
-    sub.add_argument("--pem", required=not (utils.is_arg_present("--keyfile", sys.argv)), help="🔑 the PEM file, if keyfile not provided")
+    sub.add_argument("--pem", required=check_if_sign_method_required("--pem"), help="🔑 the PEM file, if keyfile not provided")
     sub.add_argument("--pem-index", default=0, help="🔑 the index in the PEM file (default: %(default)s)")
-    sub.add_argument("--keyfile", required=not (utils.is_arg_present("--pem", sys.argv)), help="🔑 a JSON keyfile, if PEM not provided")
-    sub.add_argument("--passfile", required=not (utils.is_arg_present("--pem", sys.argv)), help="🔑 a file containing keyfile's password, if keyfile provided")
+    sub.add_argument("--keyfile", required=check_if_sign_method_required("--keyfile"), help="🔑 a JSON keyfile, if PEM not provided")
+    sub.add_argument("--passfile", required=(utils.is_arg_present("--keyfile", sys.argv)), help="🔑 a file containing keyfile's password, if keyfile provided")
+    sub.add_argument("--ledger", action="store_true", required=check_if_sign_method_required("--ledger"), default=False, help="🔐 bool flag for signing transaction using ledger")
+    sub.add_argument("--ledger-account-index", type=int, default=0, help="🔐 the index of the account when using Ledger")
+    sub.add_argument("--ledger-address-index", type=int, default=0, help="🔐 the index of the address when using Ledger")
     sub.add_argument("--sender-username", required=False, help="🖄 the username of the sender")
 
 
 def add_proxy_arg(sub: Any):
-    sub.add_argument("--proxy", default=scope.get_proxy(), help="🖧 the URL of the proxy (default: %(default)s)")
+    sub.add_argument("--proxy", default=scope.get_proxy(), help="🔗 the URL of the proxy (default: %(default)s)")
 
 
 def add_outfile_arg(sub: Any, what: str = ""):
@@ -109,6 +115,9 @@ def prepare_nonce_in_args(args: Any):
             account = Account(pem_file=args.pem, pem_index=args.pem_index)
         elif args.keyfile and args.passfile:
             account = Account(key_file=args.keyfile, pass_file=args.passfile)
+        elif args.ledger:
+            address = do_get_ledger_address(account_index=args.ledger_account_index, address_index=args.ledger_address_index)
+            account = Account(address=address)
         else:
             raise errors.NoWalletProvided()
 
@@ -138,3 +147,17 @@ def send_or_simulate(tx: Transaction, args: Any):
     elif args.simulate:
         response = tx.simulate(ElrondProxy(args.proxy))
         utils.dump_out_json(response)
+
+
+def check_if_sign_method_required(checked_method: string) -> bool:
+    methods = ["--pem", "--keyfile", "--ledger"]
+    rest_of_methods = []
+    for method in methods:
+        if method != checked_method:
+            rest_of_methods.append(method)
+
+    for method in rest_of_methods:
+        if utils.is_arg_present(method, sys.argv):
+            return False
+
+    return True
diff --git a/erdpy/cli_transactions.py b/erdpy/cli_transactions.py
index e658583b..fcffc576 100644
--- a/erdpy/cli_transactions.py
+++ b/erdpy/cli_transactions.py
@@ -2,6 +2,7 @@
 from typing import Any
 
 from erdpy import cli_shared, utils
+from erdpy.ledger.ledger_functions import do_get_ledger_address
 from erdpy.proxy.core import ElrondProxy
 from erdpy.transactions import Transaction, do_prepare_transaction
 
@@ -51,6 +52,9 @@ def create_transaction(args: Any):
     cli_shared.check_broadcast_args(args)
     cli_shared.prepare_nonce_in_args(args)
 
+    if args.ledger:
+        args.ledger_address = do_get_ledger_address(account_index=args.ledger_account_index, address_index=args.ledger_address_index)
+
     if args.data_file:
         args.data = utils.read_file(args.data_file)
 
diff --git a/erdpy/config.py b/erdpy/config.py
index dacd9155..65852de0 100644
--- a/erdpy/config.py
+++ b/erdpy/config.py
@@ -54,7 +54,9 @@ def get_dependency_url(key: str, tag: str, platform: str) -> str:
 def get_value(name: str) -> str:
     _guard_valid_name(name)
     data = get_active()
-    return data.get(name, get_defaults()[name])
+    value = data.get(name, get_defaults()[name])
+    assert isinstance(value, str)
+    return value
 
 
 def set_value(name: str, value: Any):
@@ -133,6 +135,7 @@ def get_defaults() -> Dict[str, Any]:
         "chainID": "T",
         "txVersion": "1",
         "dependencies.arwentools.tag": "v1.1.2",
+        "dependencies.elrond_wasm_rs.tag": "v0.12.0",
         "dependencies.arwentools.urlTemplate.linux": "https://github.com/ElrondNetwork/arwen-wasm-vm/archive/{TAG}.tar.gz",
         "dependencies.arwentools.urlTemplate.osx": "https://github.com/ElrondNetwork/arwen-wasm-vm/archive/{TAG}.tar.gz",
         "dependencies.llvm.tag": "v9-19feb",
@@ -155,7 +158,6 @@ def get_defaults() -> Dict[str, Any]:
         "dependencies.mcl_signer.tag": "v1.0.0",
         "dependencies.mcl_signer.urlTemplate.linux": "https://github.com/ElrondNetwork/elrond-sdk-go-tools/releases/download/{TAG}/mcl_signer_{TAG}_ubuntu-latest.tar.gz",
         "dependencies.mcl_signer.urlTemplate.osx": "https://github.com/ElrondNetwork/elrond-sdk-go-tools/releases/download/{TAG}/mcl_signer_{TAG}_macos-latest.tar.gz",
-        "dependencies.elrond_wasm_rs.tag": "v0.10.2",
     }
 
 
diff --git a/erdpy/contracts.py b/erdpy/contracts.py
index 1f713765..d65f483f 100644
--- a/erdpy/contracts.py
+++ b/erdpy/contracts.py
@@ -1,34 +1,44 @@
 import base64
 import logging
-from typing import Any, List
+from typing import Any, List, Optional
 
 from Cryptodome.Hash import keccak
 
-from erdpy import config, constants, errors, utils
-from erdpy.accounts import Address
+from erdpy import config, constants, errors
+from erdpy.accounts import Account, Address
+from erdpy.interfaces import IElrondProxy
 from erdpy.transactions import Transaction
 
 logger = logging.getLogger("contracts")
 
+HEX_PREFIX = "0X"
+
+
+class QueryResult:
+    def __init__(self, as_base64: str, as_hex: str, as_number: int):
+        self.base64 = as_base64
+        self.hex = as_hex
+        self.number = as_number
+
 
 class SmartContract:
-    def __init__(self, address=None, bytecode=None, metadata=None):
+    def __init__(self, address: Optional[Address] = None, bytecode=None, metadata=None):
         self.address = Address(address)
         self.bytecode = bytecode
         self.metadata = metadata or CodeMetadata()
 
-    def deploy(self, owner, arguments, gas_price, gas_limit, value, chain, version) -> Transaction:
+    def deploy(self, owner: Account, arguments: List[Any], gas_price: int, gas_limit: int, value: int, chain: str, version: int) -> Transaction:
         self.owner = owner
         self.compute_address()
 
         arguments = arguments or []
         gas_price = int(gas_price)
         gas_limit = int(gas_limit)
-        value = str(value or "0")
+        value = value or 0
 
         tx = Transaction()
         tx.nonce = owner.nonce
-        tx.value = value
+        tx.value = str(value)
         tx.sender = owner.address.bech32()
         tx.receiver = Address.zero().bech32()
         tx.gasPrice = gas_price
@@ -40,7 +50,7 @@ def deploy(self, owner, arguments, gas_price, gas_limit, value, chain, version)
         tx.sign(owner)
         return tx
 
-    def prepare_deploy_transaction_data(self, arguments):
+    def prepare_deploy_transaction_data(self, arguments: List[Any]):
         tx_data = f"{self.bytecode}@{constants.VM_TYPE_ARWEN}@{self.metadata.to_hex()}"
 
         for arg in arguments:
@@ -59,17 +69,17 @@ def compute_address(self):
         address = bytes([0] * 8) + bytes([5, 0]) + address[10:30] + owner_bytes[30:]
         self.address = Address(address)
 
-    def execute(self, caller, function, arguments, gas_price, gas_limit, value, chain, version) -> Transaction:
+    def execute(self, caller: Account, function: str, arguments: List[str], gas_price: int, gas_limit: int, value: int, chain: str, version: int) -> Transaction:
         self.caller = caller
 
         arguments = arguments or []
         gas_price = int(gas_price)
         gas_limit = int(gas_limit)
-        value = str(value or "0")
+        value = value or 0
 
         tx = Transaction()
         tx.nonce = caller.nonce
-        tx.value = value
+        tx.value = str(value)
         tx.sender = caller.address.bech32()
         tx.receiver = self.address.bech32()
         tx.gasPrice = gas_price
@@ -81,7 +91,7 @@ def execute(self, caller, function, arguments, gas_price, gas_limit, value, chai
         tx.sign(caller)
         return tx
 
-    def prepare_execute_transaction_data(self, function, arguments):
+    def prepare_execute_transaction_data(self, function: str, arguments: List[Any]):
         tx_data = function
 
         for arg in arguments:
@@ -89,17 +99,17 @@ def prepare_execute_transaction_data(self, function, arguments):
 
         return tx_data
 
-    def upgrade(self, owner, arguments, gas_price, gas_limit, value, chain, version) -> Transaction:
+    def upgrade(self, owner: Account, arguments: List[Any], gas_price: int, gas_limit: int, value: int, chain: str, version: int) -> Transaction:
         self.owner = owner
 
         arguments = arguments or []
         gas_price = int(gas_price or config.DEFAULT_GAS_PRICE)
         gas_limit = int(gas_limit)
-        value = str(value or "0")
+        value = value or 0
 
         tx = Transaction()
         tx.nonce = owner.nonce
-        tx.value = value
+        tx.value = str(value)
         tx.sender = owner.address.bech32()
         tx.receiver = self.address.bech32()
         tx.gasPrice = gas_price
@@ -111,7 +121,7 @@ def upgrade(self, owner, arguments, gas_price, gas_limit, value, chain, version)
         tx.sign(owner)
         return tx
 
-    def prepare_upgrade_transaction_data(self, arguments):
+    def prepare_upgrade_transaction_data(self, arguments: List[Any]):
         tx_data = f"upgradeContract@{self.bytecode}@{self.metadata.to_hex()}"
 
         for arg in arguments:
@@ -119,22 +129,37 @@ def prepare_upgrade_transaction_data(self, arguments):
 
         return tx_data
 
-    def query(self, proxy, function, arguments) -> List[Any]:
+    def query(
+        self,
+        proxy: IElrondProxy,
+        function: str,
+        arguments: List[Any],
+        value: int = 0,
+        caller: Optional[Address] = None
+    ) -> List[Any]:
+        response_data = self.query_detailed(proxy, function, arguments, value, caller)
+        return_data = response_data.get("returnData", []) or response_data.get("ReturnData", [])
+        return [self._interpret_return_data(data) for data in return_data]
+
+    def query_detailed(self, proxy: IElrondProxy, function: str, arguments: List[Any], value: int = 0, caller: Optional[Address] = None) -> Any:
         arguments = arguments or []
         prepared_arguments = [_prepare_argument(argument) for argument in arguments]
 
         payload = {
-            "ScAddress": self.address.bech32(),
-            "FuncName": function,
-            "Args": prepared_arguments
+            "scAddress": self.address.bech32(),
+            "funcName": function,
+            "args": prepared_arguments,
+            "value": str(value)
         }
 
+        if caller:
+            payload["caller"] = caller.bech32()
+
         response = proxy.query_contract(payload)
         response_data = response.get("data", {})
-        return_data = response_data.get("returnData", response_data.get("ReturnData")) or []
-        return [self._interpret_return_data(data) for data in return_data]
+        return response_data
 
-    def _interpret_return_data(self, data):
+    def _interpret_return_data(self, data: str) -> Any:
         if not data:
             return data
 
@@ -143,36 +168,50 @@ def _interpret_return_data(self, data):
             as_hex = as_bytes.hex()
             as_number = int(as_hex, 16)
 
-            result = utils.Object()
-            result.base64 = data
-            result.hex = as_hex
-            result.number = as_number
+            result = QueryResult(data, as_hex, as_number)
             return result
         except Exception:
             logger.warn(f"Cannot interpret return data: {data}")
             return None
 
 
-def _prepare_argument(argument):
-    hex_prefix = "0X"
+def _prepare_argument(argument: Any):
     as_string = str(argument).upper()
 
-    if as_string.startswith(hex_prefix):
-        return as_string[len(hex_prefix):]
+    if as_string.startswith(HEX_PREFIX):
+        return _prepare_hexadecimal(as_string)
+
+    return _prepare_decimal(as_string)
+
 
-    if not as_string.isnumeric():
-        raise errors.UnknownArgumentFormat(as_string)
+def _prepare_hexadecimal(argument: str) -> str:
+    argument = argument[len(HEX_PREFIX):]
+    argument = ensure_even_length(argument)
+    try:
+        _ = int(argument, 16)
+    except ValueError:
+        raise errors.UnknownArgumentFormat(argument)
+    return argument
 
-    as_number = int(as_string)
-    as_hexstring = hex(as_number)[len(hex_prefix):]
-    if len(as_hexstring) % 2 == 1:
-        as_hexstring = "0" + as_hexstring
 
+def _prepare_decimal(argument: str) -> str:
+    if not argument.isnumeric():
+        raise errors.UnknownArgumentFormat(argument)
+
+    as_number = int(argument)
+    as_hexstring = hex(as_number)[len(HEX_PREFIX):]
+    as_hexstring = ensure_even_length(as_hexstring)
     return as_hexstring
 
 
+def ensure_even_length(string: str) -> str:
+    if len(string) % 2 == 1:
+        return '0' + string
+    return string
+
+
 class CodeMetadata:
-    def __init__(self, upgradeable=True, payable=False):
+    def __init__(self, upgradeable: bool = True, payable: bool = False):
         self.upgradeable = upgradeable
         self.payable = payable
 
diff --git a/erdpy/conv/__init__.py b/erdpy/conv/__init__.py
deleted file mode 100644
index 42030339..00000000
--- a/erdpy/conv/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from erdpy.conv.conv import Converters
-
-__all__ = ["Converters"]
diff --git a/erdpy/conv/conv.py b/erdpy/conv/conv.py
deleted file mode 100644
index 15c68a66..00000000
--- a/erdpy/conv/conv.py
+++ /dev/null
@@ -1,21 +0,0 @@
-import binascii
-
-
-class Converters:
-    @staticmethod
-    def str_int_to_hex_str(number_str: str) -> str:
-        num_of_bytes = 1
-        if len(number_str) > 2:
-            num_of_bytes = int(len(number_str) / 2)
-        int_str = int(number_str)
-        int_bytes = int_str.to_bytes(num_of_bytes, byteorder="big")
-        bytes_str = binascii.hexlify(int_bytes).decode()
-        return bytes_str
-
-    @staticmethod
-    def parse_keys(bls_public_keys):
-        keys = bls_public_keys.split(',')
-        parsed_keys = ''
-        for key in keys:
-            parsed_keys += '@' + key
-        return parsed_keys, len(keys)
diff --git a/erdpy/delegation/staking_provider.py b/erdpy/delegation/staking_provider.py
index d295569f..7edda223 100644
--- a/erdpy/delegation/staking_provider.py
+++ b/erdpy/delegation/staking_provider.py
@@ -3,7 +3,7 @@
 from os import path
 
 from erdpy.validators.validators_file import ValidatorsFile
-from erdpy.conv.conv import Converters
+from erdpy import utils
 from erdpy.accounts import Account
 from erdpy.config import MetaChainSystemSCsCost
 from erdpy.validators.core import estimate_system_sc_call
@@ -15,8 +15,8 @@
 
 def prepare_args_for_create_new_staking_contract(args: Any):
     args.data = 'createNewDelegationContract'
-    args.data += '@' + Converters.str_int_to_hex_str(str(args.total_delegation_cap))
-    args.data += '@' + Converters.str_int_to_hex_str(str(args.service_fee))
+    args.data += '@' + utils.str_int_to_hex_str(str(args.total_delegation_cap))
+    args.data += '@' + utils.str_int_to_hex_str(str(args.service_fee))
 
     args.receiver = DELEGATION_MANAGER_SC_ADDRESS
 
@@ -57,33 +57,33 @@ def prepare_args_for_remove_nodes(args: Any):
 
 
 def prepare_args_for_stake_nodes(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.bls_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.bls_keys)
     args.data = 'stakeNodes' + parsed_keys
     args.receiver = args.delegation_contract
 
     if args.estimate_gas:
-        args.gas_limit = estimate_system_sc_call(args, MetaChainSystemSCsCost.DELEGATION_OPS
-                                                 + MetaChainSystemSCsCost.STAKE, num_keys + 1)
+        cost = MetaChainSystemSCsCost.DELEGATION_OPS + MetaChainSystemSCsCost.STAKE
+        args.gas_limit = estimate_system_sc_call(args, cost, num_keys + 1)
 
 
 def prepare_args_for_unbond_nodes(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.bls_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.bls_keys)
     args.data = 'unBondNodes' + parsed_keys
     args.receiver = args.delegation_contract
 
     if args.estimate_gas:
-        args.gas_limit = estimate_system_sc_call(args, MetaChainSystemSCsCost.DELEGATION_OPS
-                                                 + MetaChainSystemSCsCost.UNBOND, num_keys + 1)
+        cost = MetaChainSystemSCsCost.DELEGATION_OPS + MetaChainSystemSCsCost.UNBOND
+        args.gas_limit = estimate_system_sc_call(args, cost, num_keys + 1)
 
 
 def prepare_args_for_unstake_nodes(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.bls_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.bls_keys)
     args.data = 'unStakeNodes' + parsed_keys
     args.receiver = args.delegation_contract
 
     if args.estimate_gas:
-        args.gas_limit = estimate_system_sc_call(args, MetaChainSystemSCsCost.DELEGATION_OPS
-                                                 + MetaChainSystemSCsCost.UNSTAKE, num_keys + 1)
+        cost = MetaChainSystemSCsCost.DELEGATION_OPS + MetaChainSystemSCsCost.UNSTAKE
+        args.gas_limit = estimate_system_sc_call(args, cost, num_keys + 1)
 
 
 def prepare_args_for_unjail_nodes(args: Any):
@@ -91,7 +91,7 @@ def prepare_args_for_unjail_nodes(args: Any):
 
 
 def _prepare_args(command: str, args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.bls_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.bls_keys)
     args.data = command + parsed_keys
     args.receiver = args.delegation_contract
 
@@ -101,7 +101,7 @@ def _prepare_args(command: str, args: Any):
 
 def prepare_args_change_service_fee(args: Any):
     data = 'changeServiceFee'
-    data += '@' + Converters.str_int_to_hex_str(str(args.service_fee))
+    data += '@' + utils.str_int_to_hex_str(str(args.service_fee))
 
     args.data = data
     args.receiver = args.delegation_contract
@@ -111,7 +111,7 @@ def prepare_args_change_service_fee(args: Any):
 
 def prepare_args_modify_delegation_cap(args: Any):
     data = 'modifyTotalDelegationCap'
-    data += '@' + Converters.str_int_to_hex_str(str(args.delegation_cap))
+    data += '@' + utils.str_int_to_hex_str(str(args.delegation_cap))
 
     args.data = data
     args.receiver = args.delegation_contract
diff --git a/erdpy/dependencies/__init__.py b/erdpy/dependencies/__init__.py
index 7946cd0a..ba236237 100644
--- a/erdpy/dependencies/__init__.py
+++ b/erdpy/dependencies/__init__.py
@@ -1,3 +1,3 @@
-from erdpy.dependencies.install import install_module, get_module_directory, get_module_by_key
+from erdpy.dependencies.install import install_module, get_module_directory, get_module_by_key, get_golang
 
-__all__ = ["install_module", "get_module_directory", "get_module_by_key"]
+__all__ = ["install_module", "get_module_directory", "get_module_by_key", "get_golang"]
diff --git a/erdpy/dependencies/install.py b/erdpy/dependencies/install.py
index 8433ace6..5c6c3256 100644
--- a/erdpy/dependencies/install.py
+++ b/erdpy/dependencies/install.py
@@ -49,3 +49,9 @@ def get_all_deps() -> List[DependencyModule]:
         GolangModule(key="golang"),
         MclSignerModule(key="mcl_signer")
     ]
+
+
+def get_golang() -> GolangModule:
+    golang = get_module_by_key('golang')
+    assert isinstance(golang, GolangModule)
+    return golang
diff --git a/erdpy/dependencies/modules.py b/erdpy/dependencies/modules.py
index babb80d5..547ca4ae 100644
--- a/erdpy/dependencies/modules.py
+++ b/erdpy/dependencies/modules.py
@@ -57,8 +57,7 @@ def get_env(self) -> Dict[str, str]:
 
 
 class StandaloneModule(DependencyModule):
-
-    def __init__(self, key: str, aliases: List[str] = None, repo_name = None):
+    def __init__(self, key: str, aliases: List[str] = None, repo_name=None):
         if aliases is None:
             aliases = list()
 
@@ -162,9 +161,7 @@ def make_binary_symlink_in_parent_folder(self, tag, binary_name, symlink_name):
         parent = Path(self.get_parent_directory())
         symlink = parent / symlink_name
 
-        if symlink.exists():
-            symlink.unlink()
-
+        symlink.unlink(missing_ok=True)
         symlink.symlink_to(binary)
 
     def get_env(self):
diff --git a/erdpy/dispatcher/transactions/queue.py b/erdpy/dispatcher/transactions/queue.py
index 22c5081d..1d0d515f 100644
--- a/erdpy/dispatcher/transactions/queue.py
+++ b/erdpy/dispatcher/transactions/queue.py
@@ -137,7 +137,9 @@ def dispatch_transactions(self, args):
                 tx.get("value"),
                 tx.get("data"),
                 tx.get("gasPrice"),
-                tx.get("gasLimit")
+                tx.get("gasLimit"),
+                tx.get("chain"),
+                tx.get("version")
             )
             # increment nonce
             nonce += 1
diff --git a/erdpy/dns.py b/erdpy/dns.py
index 8592160c..b1452bf1 100644
--- a/erdpy/dns.py
+++ b/erdpy/dns.py
@@ -7,6 +7,7 @@
 from erdpy.contracts import SmartContract
 from erdpy.proxy.core import ElrondProxy
 from erdpy.transactions import do_prepare_transaction
+from erdpy.interfaces import IElrondProxy, ITransaction
 
 MaxNumShards = 256
 ShardIdentiferLen = 2
@@ -75,7 +76,7 @@ def registration_cost(shard_id: int, proxy: ElrondProxy) -> int:
         return int("0x{}".format(result[0]))
 
 
-def version(shard_id: int, proxy: ElrondProxy) -> str:
+def version(shard_id: int, proxy: IElrondProxy) -> str:
     dns_address = compute_dns_address_for_shard_id(shard_id)
     contract = SmartContract(dns_address)
     result = contract.query(proxy, "version", [])
diff --git a/erdpy/errors.py b/erdpy/errors.py
index 5c58f4be..04331857 100644
--- a/erdpy/errors.py
+++ b/erdpy/errors.py
@@ -178,3 +178,8 @@ def __init__(self):
 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/erdpy/interfaces.py b/erdpy/interfaces.py
index 4eb88946..dfe32366 100644
--- a/erdpy/interfaces.py
+++ b/erdpy/interfaces.py
@@ -22,6 +22,9 @@ class ITransaction:
     def serialize(self) -> bytes:
         return bytes()
 
+    def serialize_as_inner(self) -> str:
+        return ''
+
     def to_dictionary(self) -> Dict[str, Any]:
         return {}
 
@@ -36,8 +39,14 @@ def get_account_nonce(self, address: IAddress) -> int:
     def send_transaction(self, payload: Any) -> str:
         return ""
 
+    def simulate_transaction(self, payload: Any) -> str:
+        return ""
+
     def send_transactions(self, payload: List[Any]) -> Tuple[int, List[str]]:
         return 0, []
 
-    def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout) -> str:
-        return ""
+    def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout: int) -> Dict[str, Any]:
+        return dict()
+
+    def query_contract(self, payload: Any) -> Any:
+        return dict()
diff --git a/erdpy/ledger/__init__.py b/erdpy/ledger/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/erdpy/ledger/config.py b/erdpy/ledger/config.py
new file mode 100644
index 00000000..89aa9152
--- /dev/null
+++ b/erdpy/ledger/config.py
@@ -0,0 +1,38 @@
+class ElrondLedgerAppConfiguration:
+    data_activated: bool
+    account_index: int
+    address_index: int
+    version: str
+
+
+def load_ledger_config_from_response(response: bytes) -> ElrondLedgerAppConfiguration:
+    config = ElrondLedgerAppConfiguration()
+
+    config.data_activated = False
+    if response[0] == 0x01:
+        config.data_activated = True
+
+    config.account_index = response[1]
+    config.address_index = response[2]
+
+    version = str(response[3]) + "." + str(response[4]) + "." + str(response[5])
+    config.version = version
+
+    return config
+
+
+def compare_versions(version1: str, version2: str) -> int:
+    version1_tuple = version_tuple(version1)
+    version2_tuple = version_tuple(version2)
+    if version1_tuple == version2_tuple:
+        return 0
+    if version1_tuple < version2_tuple:
+        return -1
+    return 1
+
+
+def version_tuple(v):
+    filled = []
+    for point in v.split("."):
+        filled.append(point.zfill(8))
+    return tuple(filled)
diff --git a/erdpy/ledger/ledger_app_handler.py b/erdpy/ledger/ledger_app_handler.py
new file mode 100644
index 00000000..10472307
--- /dev/null
+++ b/erdpy/ledger/ledger_app_handler.py
@@ -0,0 +1,146 @@
+from ledgercomm import Transport
+
+from erdpy.errors import LedgerError
+from erdpy.ledger.config import load_ledger_config_from_response, ElrondLedgerAppConfiguration
+
+SIGN_USING_HASH_VERSION = "1.0.11"
+
+
+class Apdu:
+    cla: int
+    ins: int
+    p1: int
+    p2: int
+    data: bytes
+
+
+class ElrondLedgerApp:
+    def __init__(self):
+        self.transport = Transport(interface="hid", debug=False)  # Nano S/X using HID interface
+
+    def close(self):
+        self.transport.close()
+
+    def set_address(self, account_index=0, address_index=0):
+        data = account_index.to_bytes(4, byteorder='big') + address_index.to_bytes(4, byteorder='big')
+        self.transport.send(cla=0xed, ins=0x05, p1=0x00, p2=0x00, cdata=data)
+        sw, response = self.transport.recv()
+        err = get_error(sw)
+        if err != '':
+            raise LedgerError(err)
+
+    def get_address(self, account_index=0, address_index=0) -> str:
+        data = account_index.to_bytes(4, byteorder='big') + address_index.to_bytes(4, byteorder='big')
+
+        self.transport.send(cla=0xed, ins=0x03, p1=0x00, p2=0x00, cdata=data)
+        sw, response = self.transport.recv()
+        assert isinstance(response, bytes)
+
+        err = get_error(sw)
+        if err != '':
+            raise LedgerError(err)
+
+        response_body = response[1:]
+        address = response_body.decode("utf-8")
+        return address
+
+    def get_app_configuration(self) -> ElrondLedgerAppConfiguration:
+        self.transport.send(cla=0xed, ins=0x02, p1=0x00, p2=0x00, cdata=b"")
+        sw, response = self.transport.recv()
+        err = get_error(sw)
+        if err != '':
+            raise LedgerError(err)
+        return load_ledger_config_from_response(response)
+
+    def get_version(self) -> str:
+        config = self.get_app_configuration()
+        return config.version
+
+    def sign_transaction(self, marshaled_tx: bytes, should_use_hash_signing: bool) -> str:
+        total_size = len(marshaled_tx)
+        max_chunk_size = 150
+
+        apdus = list()
+
+        offset = 0
+        while offset != total_size:
+            is_first = offset == 0
+
+            apdu = Apdu()
+
+            if is_first:
+                apdu.p1 = 0x00
+            else:
+                apdu.p1 = 0x80
+
+            has_more = offset + max_chunk_size < total_size
+            chunk_size = total_size - offset
+            if has_more:
+                chunk_size = max_chunk_size
+
+            ins_signing_method = 0x04
+            if should_use_hash_signing:
+                ins_signing_method = 0x07
+
+            apdu.ins = ins_signing_method
+            apdu.p2 = 0x00
+            apdu.cla = 0xed
+            apdu.data = marshaled_tx[offset:offset + chunk_size]
+
+            apdus.append(apdu)
+
+            offset += chunk_size
+
+        return self.get_signature_from_apdus(apdus)
+
+    def get_signature_from_apdus(self, apdus) -> str:
+        sw: int
+        response: bytes
+        for apdu in apdus:
+            self.transport.send(
+                cla=apdu.cla,
+                ins=apdu.ins,
+                p1=apdu.p1,
+                p2=apdu.p2,
+                cdata=apdu.data)
+            sw, response = self.transport.recv()
+
+        assert isinstance(response, bytes)
+        if len(response) != 65 or response[0] != 64 or get_error(sw) != '':
+            err_message = "signature failed"
+            err = get_error(sw)
+            if err != '':
+                err_message += ': ' + err
+            raise LedgerError(err_message)
+
+        response_body = response[1:]
+        signature = response_body.hex()
+        return signature
+
+
+def get_error(code):
+    switcher = {
+        0x9000: '',
+        0x6985: 'user denied',
+        0x6D00: 'unknown instruction',
+        0x6E00: 'wrong cla',
+        0x6E10: 'signature failed',
+        0x6E01: 'invalid arguments',
+        0x6E02: 'invalid message',
+        0x6E03: 'invalid p1',
+        0x6E04: 'message too long',
+        0x6E05: 'receiver too long',
+        0x6E06: 'amount too long',
+        0x6E07: 'contract data disabled',
+        0x6E08: 'message incomplete',
+        0x6E09: 'wrong tx version',
+        0x6E0A: 'nonce too long',
+        0x6E0B: 'invalid amount',
+        0x6E0C: 'invalid fee',
+        0x6E0D: 'pretty failed',
+        0x6E0E: 'data too long',
+        0x6E0F: 'wrong tx options',
+        0x6E11: 'regular signing is deprecated',
+    }
+
+    return switcher.get(code, "unknown error code: " + hex(code))
diff --git a/erdpy/ledger/ledger_functions.py b/erdpy/ledger/ledger_functions.py
new file mode 100644
index 00000000..f27b702f
--- /dev/null
+++ b/erdpy/ledger/ledger_functions.py
@@ -0,0 +1,35 @@
+from erdpy.ledger.ledger_app_handler import ElrondLedgerApp
+
+TX_HASH_SIGN_VERSION = 2
+TX_HASH_SIGN_OPTIONS = 1
+
+
+def do_sign_transaction_with_ledger(
+    tx_payload: bytes,
+    account_index: int,
+    address_index: int,
+    sign_using_hash: bool
+) -> str:
+    ledger_handler = ElrondLedgerApp()
+    ledger_handler.set_address(account_index=account_index, address_index=address_index)
+
+    signature = ledger_handler.sign_transaction(tx_payload, sign_using_hash)
+    ledger_handler.close()
+
+    return signature
+
+
+def do_get_ledger_address(account_index: int, address_index: int) -> str:
+    ledger_handler = ElrondLedgerApp()
+    ledger_address = ledger_handler.get_address(account_index=account_index, address_index=address_index)
+    ledger_handler.close()
+
+    return ledger_address
+
+
+def do_get_ledger_version() -> str:
+    ledger_handler = ElrondLedgerApp()
+    ledger_version = ledger_handler.get_version()
+    ledger_handler.close()
+
+    return ledger_version
diff --git a/erdpy/myprocess.py b/erdpy/myprocess.py
index cdba9a8a..976f64dd 100644
--- a/erdpy/myprocess.py
+++ b/erdpy/myprocess.py
@@ -2,14 +2,17 @@
 import logging
 import subprocess
 import traceback
-from typing import Any, List
+from pathlib import Path
+from typing import Any, List, Optional, Union
 
 from erdpy import errors
 
 logger = logging.getLogger("myprocess")
 
+ReturnCode = int
 
-def run_process(args: List[str], env: Any = None, dump_to_stdout: bool = True, cwd: str = None):
+
+def run_process(args: List[str], env: Any = None, dump_to_stdout: bool = True, cwd: Optional[Union[str, Path]] = None):
     logger.info(f"run_process: {args}, in folder: {cwd}")
 
     try:
@@ -22,7 +25,13 @@ def run_process(args: List[str], env: Any = None, dump_to_stdout: bool = True, c
         raise errors.ExternalProcessError(error.cmd, error.output)
 
 
-def run_process_async(args: List[str], env: Any = None, cwd: str = None, stdout_sink=None, stderr_sink=None):
+def run_process_async(
+    args: List[str],
+    env: Any = None,
+    cwd: Optional[str] = None,
+    stdout_sink=None,
+    stderr_sink=None
+) -> ReturnCode:
     loop = asyncio.get_event_loop()
     result = loop.run_until_complete(_async_subprocess(args, env, stdout_sink, stderr_sink, cwd))
     loop.close()
@@ -30,7 +39,7 @@ def run_process_async(args: List[str], env: Any = None, cwd: str = None, stdout_
     return result
 
 
-async def _async_subprocess(args, env, stdout_sink, stderr_sink, cwd: str):
+async def _async_subprocess(args, env, stdout_sink, stderr_sink, cwd: Optional[str]) -> ReturnCode:
     process = await asyncio.create_subprocess_exec(*args, env=env, stdout=asyncio.subprocess.PIPE,
                                                    stderr=asyncio.subprocess.PIPE, cwd=cwd)
 
diff --git a/erdpy/projects/project_base.py b/erdpy/projects/project_base.py
index bddd048b..31308140 100644
--- a/erdpy/projects/project_base.py
+++ b/erdpy/projects/project_base.py
@@ -3,7 +3,7 @@
 import shutil
 from os import path
 from pathlib import Path
-from typing import List, cast
+from typing import Any, Dict, List, Union, cast
 
 from erdpy import dependencies, errors, myprocess, utils
 from erdpy.dependencies.modules import StandaloneModule
@@ -14,11 +14,11 @@
 # TODO use pathlib.Path everywhere
 class Project:
 
-    def __init__(self, directory):
+    def __init__(self, directory: Union[Path, str]):
         self.path = Path(directory).expanduser().resolve()
         self.directory = str(self.path)
 
-    def build(self, options=None):
+    def build(self, options: Union[Dict[str, Any], None] = None):
         self.options = options or dict()
         self.debug = self.options.get("debug", False)
         self._ensure_dependencies_installed()
diff --git a/erdpy/projects/project_rust.py b/erdpy/projects/project_rust.py
index 861e597d..9d30b601 100644
--- a/erdpy/projects/project_rust.py
+++ b/erdpy/projects/project_rust.py
@@ -3,7 +3,7 @@
 import subprocess
 from os import path
 from pathlib import Path
-from typing import Any, MutableMapping
+from typing import Any, Dict, cast
 
 from erdpy import dependencies, errors, myprocess, utils
 from erdpy.projects.project_base import Project
@@ -79,10 +79,10 @@ def _generate_abi(self):
         utils.prettify_json_file(self._get_abi_filepath())
 
     def _has_abi(self):
-        return Path(self._get_abi_folder(), "Cargo.toml").is_file()
+        return (self._get_abi_folder() / "Cargo.toml").exists()
 
     def _get_abi_filepath(self):
-        return Path(self._get_abi_folder(), "abi.json")
+        return self._get_abi_folder() / "abi.json"
 
     def _get_abi_folder(self):
         return Path(self.directory, "abi")
@@ -110,6 +110,8 @@ def _get_env(self):
 
 
 class CargoFile:
+    data: Dict[str, Any]
+
     def __init__(self, path):
         self.data = {}
         self.path = path
@@ -165,31 +167,34 @@ def publish(self, value):
     def save(self):
         utils.write_toml_file(self.path, self.data)
 
-    def _get_package(self) -> MutableMapping[str, Any]:
+    def _get_package(self) -> Dict[str, Any]:
         if "package" not in self.data:
             self.data["package"] = {}
-        return self.data["package"]
+        package = cast(Dict[str, Any], self.data['package'])
+        return package
 
-    def get_dependencies(self) -> MutableMapping[str, Any]:
+    def get_dependencies(self) -> Dict[str, Any]:
         if "dependencies" not in self.data:
             self.data["dependencies"] = {}
-        return self.data["dependencies"]
+        dependencies = cast(Dict[str, Any], self.data['dependencies'])
+        return dependencies
 
-    def get_dev_dependencies(self) -> MutableMapping[str, Any]:
+    def get_dev_dependencies(self) -> Dict[str, Any]:
         if "dev-dependencies" not in self.data:
             self.data["dev-dependencies"] = {}
-        return self.data["dev-dependencies"]
+        dev_dependencies = cast(Dict[str, Any], self.data['dev-dependencies'])
+        return dev_dependencies
 
-    def get_dependency(self, name) -> MutableMapping[str, Any]:
+    def get_dependency(self, name) -> Dict[str, Any]:
         dependencies = self.get_dependencies()
-        dependency = dependencies.get(name)
+        dependency = cast(Dict[str, Any], dependencies.get(name))
         if dependency is None:
             raise errors.BuildError(f"Can't get cargo dependency: {name}")
         return dependency
 
-    def get_dev_dependency(self, name) -> MutableMapping[str, Any]:
+    def get_dev_dependency(self, name) -> Dict[str, Any]:
         dependencies = self.get_dev_dependencies()
-        dependency = dependencies.get(name)
+        dependency = cast(Dict[str, Any], dependencies.get(name))
         if dependency is None:
             raise errors.BuildError(f"Can't get cargo dev-dependency: {name}")
         return dependency
diff --git a/erdpy/projects/templates.py b/erdpy/projects/templates.py
index bd2c11cf..5748752f 100644
--- a/erdpy/projects/templates.py
+++ b/erdpy/projects/templates.py
@@ -2,6 +2,8 @@
 import logging
 import os
 from os import path
+from pathlib import Path
+from typing import Union
 
 from erdpy import errors, utils
 from erdpy.projects import shared
@@ -33,10 +35,10 @@ def __init__(self, name, repository):
         self.language = repository.get_language(name)
 
 
-def create_from_template(name: str, template_name: str, directory: str):
+def create_from_template(project_name: str, template_name: str, directory: Union[Path, str]):
     directory = path.expanduser(directory)
 
-    logger.info("create_from_template.name: %s", name)
+    logger.info("create_from_template.project_name: %s", project_name)
     logger.info("create_from_template.template_name: %s", template_name)
     logger.info("create_from_template.directory: %s", directory)
 
@@ -44,7 +46,7 @@ def create_from_template(name: str, template_name: str, directory: str):
         logger.info("Using current directory")
         directory = os.getcwd()
 
-    project_directory = path.join(directory, name)
+    project_directory = path.join(directory, project_name)
     if path.exists(project_directory):
         raise errors.BadDirectory(project_directory)
 
@@ -52,7 +54,7 @@ def create_from_template(name: str, template_name: str, directory: str):
     _copy_template(template_name, project_directory)
 
     template = _load_as_template(project_directory)
-    template.apply(template_name, name)
+    template.apply(template_name, project_name)
 
     logger.info("Project created, template applied.")
 
@@ -208,11 +210,15 @@ def _patch_source_code_abi(self):
         if not path.exists(abi_main_path):
             return
 
+        template_name = self.template_name.replace('-', '_')
+        project_name = self.project_name.replace('-', '_')
+
         self._replace_in_files(
             [abi_main_path],
             [
                 # Example: replace "use simple-erc20::*" to "use my_token::*"
-                (f"use {self.template_name.replace('-', '_')}::*", f"use {self.project_name.replace('-', '_')}::*")
+                (f"use {template_name}::*", f"use {project_name}::*"),
+                (f"<{template_name}::AbiProvider>()", f"<{project_name}::AbiProvider>()")
             ]
         )
 
diff --git a/erdpy/projects/templates_config.py b/erdpy/projects/templates_config.py
index 12fedc4d..fb9473f0 100644
--- a/erdpy/projects/templates_config.py
+++ b/erdpy/projects/templates_config.py
@@ -7,7 +7,7 @@
 def get_templates_repositories():
     timestamp = int(time.time())
     examples_rs_tag = config.get_dependency_tag('elrond_wasm_rs')
-    examples_rs_tag_no_v = examples_rs_tag[1:]
+    examples_rs_tag_no_v = remove_initial_v_from_version(examples_rs_tag)
 
     return [
         TemplatesRepository(
@@ -24,3 +24,16 @@ def get_templates_repositories():
             relative_path=f"elrond-wasm-rs-{examples_rs_tag_no_v}/contracts/examples"
         )
     ]
+
+
+def remove_initial_v_from_version(version: str) -> str:
+    """Remove the initial 'v' from semver strings 'vX.XX.XX', but leave branch
+    names or non-semver tags unchanged"""
+    if version[0] != 'v':
+        return version
+
+    version_no_v = version[1:]
+    if not version_no_v[0].isnumeric():
+        return version
+
+    return version_no_v
diff --git a/erdpy/projects/templates_repository.py b/erdpy/projects/templates_repository.py
index e548c957..30f1f072 100644
--- a/erdpy/projects/templates_repository.py
+++ b/erdpy/projects/templates_repository.py
@@ -34,7 +34,6 @@ def _download_if_old(self):
 
         downloader.download(self.url, archive)
 
-
     def _get_archive_path(self):
         tools_folder = workstation.get_tools_folder()
         archive = path.join(tools_folder, f"{self.key}.zip")
diff --git a/erdpy/proxy/core.py b/erdpy/proxy/core.py
index 9148ed77..9e5aa35c 100644
--- a/erdpy/proxy/core.py
+++ b/erdpy/proxy/core.py
@@ -1,8 +1,9 @@
 import logging
 import time
-from typing import Any, List, Tuple
+from typing import Any, List, Tuple, cast, Dict
 
 from erdpy.accounts import Address
+from erdpy.interfaces import IAddress
 from erdpy.proxy.http_facade import do_get, do_post
 from erdpy.proxy.messages import NetworkConfig
 
@@ -16,19 +17,19 @@ class ElrondProxy:
     def __init__(self, url: str):
         self.url = url
 
-    def get_account_nonce(self, address: Address) -> int:
+    def get_account_nonce(self, address: IAddress) -> int:
         url = f"{self.url}/address/{address.bech32()}"
         response = do_get(url)
         nonce = response.get("account").get("nonce", 0)
         return int(nonce)
 
-    def get_account_balance(self, address: Address):
+    def get_account_balance(self, address: IAddress):
         url = f"{self.url}/address/{address.bech32()}/balance"
         response = do_get(url)
         balance = response.get("balance", 0)
         return int(balance)
 
-    def get_account(self, address: Address):
+    def get_account(self, address: IAddress):
         url = f"{self.url}/address/{address.bech32()}"
         response = do_get(url)
         account = response.get("account", dict())
@@ -48,11 +49,18 @@ def get_account_transactions(self, address: Address):
 
     def get_esdt_tokens(self, address: str) -> List:
         response = do_get(f"{self.url}/address/{address}/esdt")
-        return response.get("tokens")
+        esdts = response.get("esdts")
+        return cast(List, esdts)
 
     def get_esdt_balance(self, address: str, ticker: str) -> dict:
         response = do_get(f"{self.url}/address/{address}/esdt/{ticker}")
-        return response.get("tokenData")
+        token_data = response.get("tokenData")
+        return cast(dict, token_data)
+
+    def get_all_tokens(self) -> List[str]:
+        response = do_get(f"{self.url}/network/esdts")
+        tokens = response.get("tokens", [])
+        return cast(List[str], tokens)
 
     def get_num_shards(self):
         network_config = self.get_network_config()
@@ -91,12 +99,12 @@ def get_network_config(self) -> NetworkConfig:
     def send_transaction(self, payload: Any) -> str:
         url = f"{self.url}/transaction/send"
         response = do_post(url, payload)
-        tx_hash = response.get("txHash")
+        tx_hash = str(response.get("txHash"))
         return tx_hash
 
     def simulate_transaction(self, payload: Any) -> str:
         url = f"{self.url}/transaction/simulate"
-        response = do_post(url, payload)
+        response = str(do_post(url, payload))
         return response
 
     def send_transactions(self, payload: List[Any]) -> Tuple[int, List[str]]:
@@ -107,18 +115,19 @@ def send_transactions(self, payload: List[Any]) -> Tuple[int, List[str]]:
         hashes = response.get("txsHashes")
         return num_sent, hashes
 
-    def query_contract(self, payload: Any):
+    def query_contract(self, payload: Any) -> Any:
         url = f"{self.url}/vm-values/query"
         response = do_post(url, payload)
         return response
 
-    def get_transaction(self, tx_hash: str, sender_address: str = "", with_results: bool = False) -> Any:
+    def get_transaction(self, tx_hash: str, sender_address: str = "", with_results: bool = False) -> Dict[str, Any]:
         url = f"{self.url}/transaction/{tx_hash}"
         url += f"?sender={sender_address or ''}"
         url += f"&withResults={with_results}"
 
         response = do_get(url)
-        transaction = response.get("transaction", dict())
+        transaction_response = response.get("transaction", dict())
+        transaction = cast(Dict[str, Any], transaction_response)
         transaction['hash'] = tx_hash
         return transaction
 
@@ -131,7 +140,7 @@ def get_hyperblock(self, key) -> Any:
         response = response.get("hyperblock", {})
         return response
 
-    def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout=100) -> str:
+    def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout: int = 100) -> Dict[str, Any]:
         url = f"{self.url}/transaction/send"
         response = do_post(url, payload)
         tx_hash = response.get("txHash")
@@ -140,7 +149,7 @@ def send_transaction_and_wait_for_result(self, payload: Any, num_seconds_timeout
             if self.is_transaction_finalized(tx_hash):
                 return self.get_transaction(tx_hash=tx_hash, with_results=True)
             time.sleep(1)
-        return ""
+        return dict()
 
     def is_transaction_finalized(self, tx_hash):
         last_nonce = self.get_last_block_nonce("metachain")
diff --git a/erdpy/proxy/messages.py b/erdpy/proxy/messages.py
index 46c4a1f7..b0d2f54f 100644
--- a/erdpy/proxy/messages.py
+++ b/erdpy/proxy/messages.py
@@ -5,5 +5,5 @@ class NetworkConfig:
     def __init__(self, data: Dict[str, Any]) -> None:
         self.num_shards = data.get("erd_num_shards_without_meta", 0)
         self.min_gas_price = data.get("erd_min_gas_price", 0)
-        self.chain_id = data.get("erd_chain_id", 0)
+        self.chain_id = data.get("erd_chain_id", "?")
         self.min_tx_version = data.get("erd_min_transaction_version", 0)
diff --git a/erdpy/transactions.py b/erdpy/transactions.py
index fa4b24b2..5f6cdb63 100644
--- a/erdpy/transactions.py
+++ b/erdpy/transactions.py
@@ -2,11 +2,15 @@
 import json
 import logging
 from collections import OrderedDict
-from typing import Any, Dict
+from typing import Any, Dict, List, TextIO
 
 from erdpy import errors, utils
 from erdpy.accounts import Account, Address
 from erdpy.interfaces import IElrondProxy, ITransaction
+from erdpy.ledger.config import compare_versions
+from erdpy.ledger.ledger_app_handler import SIGN_USING_HASH_VERSION
+from erdpy.ledger.ledger_functions import do_get_ledger_version, TX_HASH_SIGN_VERSION, TX_HASH_SIGN_OPTIONS, \
+    do_sign_transaction_with_ledger
 from erdpy.wallet import signing
 
 logger = logging.getLogger("transactions")
@@ -26,6 +30,7 @@ def __init__(self):
         self.data = ""
         self.chainID = ""
         self.version = 0
+        self.options = 0
         self.signature = ""
 
     # The data field is base64-encoded. erdpy only supports utf-8 "data" at this moment.
@@ -64,19 +69,19 @@ def serialize(self) -> bytes:
         serialized = self._dict_to_json(dictionary)
         return serialized
 
-    def _dict_to_json(self, dictionary: Dict[str, Any]):
+    def _dict_to_json(self, dictionary: Dict[str, Any]) -> bytes:
         serialized = json.dumps(dictionary, separators=(',', ':')).encode("utf8")
         return serialized
 
-    def serialize_as_inner(self):
+    def serialize_as_inner(self) -> str:
         inner_dictionary = self.to_dictionary_as_inner()
         serialized = self._dict_to_json(inner_dictionary)
         serialized_hex = serialized.hex()
         return f"relayedTx@{serialized_hex}"
 
     @classmethod
-    def load_from_file(cls, f: Any):
-        data_json: bytes = utils.read_file(f).encode()
+    def load_from_file(cls, f: TextIO):
+        data_json: bytes = f.read().encode()
         fields = json.loads(data_json).get("tx")
         instance = cls()
         instance.__dict__.update(fields)
@@ -147,7 +152,12 @@ def to_dictionary(self) -> Dict[str, Any]:
             dictionary["data"] = self.data_encoded()
 
         dictionary["chainID"] = self.chainID
-        dictionary["version"] = int(self.version)
+
+        if self.version:
+            dictionary["version"] = int(self.version)
+
+        if self.options:
+            dictionary["options"] = int(self.options)
 
         if self.signature:
             dictionary["signature"] = self.signature
@@ -171,12 +181,13 @@ def wrap_inner(self, inner: ITransaction) -> None:
 
 class BunchOfTransactions:
     def __init__(self):
-        self.transactions = []
+        self.transactions: List[Transaction] = []
 
     def add_prepared(self, transaction: Transaction):
         self.transactions.append(transaction)
 
-    def add(self, sender: Account, receiver_address: str, nonce: Any, value: Any, data: str, gas_price: int, gas_limit: int, chain: str, version: int):
+    def add(self, sender: Account, receiver_address: str, nonce: Any, value: Any, data: str, gas_price: int,
+            gas_limit: int, chain: str, version: int, options: int):
         tx = Transaction()
         tx.nonce = int(nonce)
         tx.value = str(value)
@@ -187,6 +198,7 @@ def add(self, sender: Account, receiver_address: str, nonce: Any, value: Any, da
         tx.data = data
         tx.chainID = chain
         tx.version = version
+        tx.options = options
 
         tx.sign(sender)
         self.transactions.append(tx)
@@ -205,6 +217,8 @@ def send(self, proxy: IElrondProxy):
 
 def do_prepare_transaction(args: Any) -> Transaction:
     account = Account()
+    if args.ledger:
+        return do_prepare_transaction_ledger(args)
     if args.pem:
         account = Account(pem_file=args.pem, pem_index=args.pem_index)
     elif args.keyfile and args.passfile:
@@ -222,6 +236,47 @@ def do_prepare_transaction(args: Any) -> Transaction:
     tx.data = args.data
     tx.chainID = args.chain
     tx.version = int(args.version)
+    tx.options = int(args.options)
 
     tx.sign(account)
     return tx
+
+
+def do_prepare_transaction_ledger(args: Any) -> Transaction:
+    account_index = 0
+    address_index = 0
+    if args.ledger_account_index:
+        account_index = args.ledger_account_index
+    if args.ledger_address_index:
+        address_index = args.ledger_address_index
+
+    tx = Transaction()
+    tx.nonce = int(args.nonce)
+    tx.value = args.value
+    tx.receiver = args.receiver
+    tx.sender = args.ledger_address
+    tx.senderUsername = getattr(args, "sender_username", None)
+    tx.receiverUsername = getattr(args, "receiver_username", None)
+    tx.gasPrice = int(args.gas_price)
+    tx.gasLimit = int(args.gas_limit)
+    tx.data = args.data
+    tx.chainID = args.chain
+
+    ledger_version = do_get_ledger_version()
+    should_use_hash_signing = compare_versions(ledger_version, SIGN_USING_HASH_VERSION) >= 0
+    if should_use_hash_signing:
+        tx.version = TX_HASH_SIGN_VERSION
+        tx.options = TX_HASH_SIGN_OPTIONS
+
+    tx.signature = ""
+
+    signature = do_sign_transaction_with_ledger(
+        tx_payload=tx.serialize(),
+        account_index=account_index,
+        address_index=address_index,
+        sign_using_hash=should_use_hash_signing,
+    )
+
+    tx.signature = signature
+
+    return tx
diff --git a/erdpy/utils.py b/erdpy/utils.py
index a8f1d42c..e9c477f0 100644
--- a/erdpy/utils.py
+++ b/erdpy/utils.py
@@ -9,7 +9,7 @@
 import tarfile
 import zipfile
 from pathlib import Path
-from typing import Any, Dict, List, Union
+from typing import Any, List, Union, Optional, cast, IO, Dict
 
 import toml
 
@@ -75,17 +75,35 @@ def read_lines(file: str):
     return lines
 
 
-def read_file(f: Any, binary=False) -> Union[str, bytes]:
+# TODO delete this function, it is too generic
+# TODO find usages in legolas
+def read_file(f: Any, binary: bool = False) -> Union[str, bytes]:
     try:
-        mode = "rb" if binary else "r"
         if isinstance(f, str) or isinstance(f, pathlib.PosixPath):
-            with open(f, mode) as f:
-                return f.read()
-        return f.read()
+            path = Path(f)
+            if binary:
+                return read_binary_file(path)
+            return read_text_file(path)
+
+        file = cast(IO, f)
+        result = file.read()
+        assert isinstance(result, str) or isinstance(result, bytes)
+        return result
+
     except Exception as err:
         raise errors.BadFile(f, err)
 
 
+def read_binary_file(path: Path) -> bytes:
+    with open(path, 'rb') as binary_file:
+        return binary_file.read()
+
+
+def read_text_file(path: Path) -> str:
+    with open(path, 'r') as text_file:
+        return text_file.read()
+
+
 def write_file(f: Any, text: str):
     if isinstance(f, str) or isinstance(f, pathlib.PosixPath):
         with open(f, "w") as f:
@@ -102,9 +120,11 @@ def write_toml_file(filename, data):
         toml.dump(data, f)
 
 
-def read_json_file(filename: str) -> Dict[str, Any]:
+def read_json_file(filename: Union[str, Path]) -> Dict[str, Any]:
+    data: Dict[str, Any]
     with open(filename) as f:
-        return json.load(f)
+        data = json.load(f)
+    return data
 
 
 def write_json_file(filename: str, data: Any):
@@ -146,7 +166,7 @@ def find_in_dictionary(dictionary, compound_path):
     return node
 
 
-def list_files(folder: str, suffix: str = None) -> List[str]:
+def list_files(folder: Union[str, Path], suffix: Optional[str] = None) -> List[str]:
     files = os.listdir(folder)
     files = [os.path.join(folder, f) for f in files]
 
@@ -156,7 +176,7 @@ def list_files(folder: str, suffix: str = None) -> List[str]:
     return files
 
 
-def remove_folder(folder):
+def remove_folder(folder: Union[str, Path]):
     shutil.rmtree(folder, ignore_errors=True)
 
 
@@ -166,17 +186,13 @@ def symlink(real: str, link: str) -> None:
     os.symlink(real, link)
 
 
-def str_to_bool(input: str) -> bool:
-    return str(input).lower() in ["true", "1", "t", "y", "yes"]
-
-
-def as_object(input: Any) -> Object:
-    if isinstance(input, dict):
+def as_object(data: Object) -> Object:
+    if isinstance(data, dict):
         result = Object()
-        result.__dict__.update(input)
+        result.__dict__.update(data)
         return result
 
-    return input
+    return data
 
 
 def is_arg_present(key: str, args: List[str]) -> bool:
@@ -189,6 +205,24 @@ def is_arg_present(key: str, args: List[str]) -> bool:
     return False
 
 
+def str_int_to_hex_str(number_str: str) -> str:
+    num_of_bytes = 1
+    if len(number_str) > 2:
+        num_of_bytes = int(len(number_str) / 2)
+    int_str = int(number_str)
+    int_bytes = int_str.to_bytes(num_of_bytes, byteorder="big")
+    bytes_str = int_bytes.hex()
+    return bytes_str
+
+
+def parse_keys(bls_public_keys):
+    keys = bls_public_keys.split(',')
+    parsed_keys = ''
+    for key in keys:
+        parsed_keys += '@' + key
+    return parsed_keys, len(keys)
+
+
 # https://code.visualstudio.com/docs/python/debugging
 def breakpoint():
     import debugpy
diff --git a/erdpy/validators/core.py b/erdpy/validators/core.py
index 5b4a23af..42095391 100644
--- a/erdpy/validators/core.py
+++ b/erdpy/validators/core.py
@@ -4,7 +4,7 @@
 from typing import Any
 
 from erdpy.validators.validators_file import ValidatorsFile
-from erdpy.conv.conv import Converters
+from erdpy import utils
 from erdpy.accounts import Account, Address
 from erdpy.config import MetaChainSystemSCsCost, MIN_GAS_LIMIT, GAS_PER_DATA_BYTE
 from erdpy.wallet.pem import parse_validator_pem
@@ -61,7 +61,7 @@ def prepare_args_for_top_up(args: Any):
 
 
 def prepare_args_for_unstake(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.nodes_public_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.nodes_public_keys)
     args.data = 'unStake' + parsed_keys
     args.receiver = VALIDATORS_SMART_CONTRACT_ADDRESS
 
@@ -70,7 +70,7 @@ def prepare_args_for_unstake(args: Any):
 
 
 def prepare_args_for_unbond(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.nodes_public_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.nodes_public_keys)
     args.data = 'unBond' + parsed_keys
     args.receiver = VALIDATORS_SMART_CONTRACT_ADDRESS
 
@@ -79,7 +79,7 @@ def prepare_args_for_unbond(args: Any):
 
 
 def prepare_args_for_unjail(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.nodes_public_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.nodes_public_keys)
     args.data = 'unJail' + parsed_keys
     args.receiver = VALIDATORS_SMART_CONTRACT_ADDRESS
 
@@ -105,7 +105,7 @@ def prepare_args_for_claim(args: Any):
 
 
 def prepare_args_for_unstake_nodes(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.nodes_public_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.nodes_public_keys)
     args.data = 'unStakeNodes' + parsed_keys
 
     args.receiver = VALIDATORS_SMART_CONTRACT_ADDRESS
@@ -115,7 +115,7 @@ def prepare_args_for_unstake_nodes(args: Any):
 
 def prepare_args_for_unstake_tokens(args: Any):
     args.data = 'unStakeTokens'
-    args.data += '@' + Converters.str_int_to_hex_str(str(args.unstake_value))
+    args.data += '@' + utils.str_int_to_hex_str(str(args.unstake_value))
 
     args.receiver = VALIDATORS_SMART_CONTRACT_ADDRESS
     if args.estimate_gas:
@@ -123,7 +123,7 @@ def prepare_args_for_unstake_tokens(args: Any):
 
 
 def prepare_args_for_unbond_nodes(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.nodes_public_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.nodes_public_keys)
     args.data = 'unBondNodes' + parsed_keys
 
     args.receiver = VALIDATORS_SMART_CONTRACT_ADDRESS
@@ -133,7 +133,7 @@ def prepare_args_for_unbond_nodes(args: Any):
 
 def prepare_args_for_unbond_tokens(args: Any):
     args.data = 'unBondTokens'
-    args.data += '@' + Converters.str_int_to_hex_str(str(args.unbond_value))
+    args.data += '@' + utils.str_int_to_hex_str(str(args.unbond_value))
 
     args.receiver = VALIDATORS_SMART_CONTRACT_ADDRESS
     if args.estimate_gas:
@@ -149,7 +149,7 @@ def prepare_args_for_clean_registered_data(args: Any):
 
 
 def prepare_args_for_restake_unstaked_nodes(args: Any):
-    parsed_keys, num_keys = Converters.parse_keys(args.nodes_public_keys)
+    parsed_keys, num_keys = utils.parse_keys(args.nodes_public_keys)
     args.data = 'reStakeUnStakedNodes' + parsed_keys
 
     args.receiver = VALIDATORS_SMART_CONTRACT_ADDRESS
diff --git a/erdpy/wallet/pem.py b/erdpy/wallet/pem.py
index d1bb9a89..449df248 100644
--- a/erdpy/wallet/pem.py
+++ b/erdpy/wallet/pem.py
@@ -3,7 +3,7 @@
 import textwrap
 from os import path
 from pathlib import Path
-from typing import Tuple, Union
+from typing import List, Tuple, Union
 
 from erdpy import guards, utils
 
@@ -31,6 +31,28 @@ def parse(pem_file: Union[str, Path], index: int = 0) -> Tuple[bytes, bytes]:
     return seed, pubkey
 
 
+def parse_all(pem_file: Union[str, Path]) -> List[Tuple[bytes, bytes]]:
+    pem_file = path.expanduser(pem_file)
+    guards.is_file(pem_file)
+
+    lines = utils.read_lines(pem_file)
+    keys_lines = [list(key_lines) for is_next_key, key_lines in itertools.groupby(lines, lambda line: "-----" in line)
+                  if not is_next_key]
+    keys = ["".join(key_lines) for key_lines in keys_lines]
+
+    result = []
+
+    for key_base64 in keys:
+        key_hex = base64.b64decode(key_base64).decode()
+        key_bytes = bytes.fromhex(key_hex)
+        seed = key_bytes[:32]
+        pubkey = key_bytes[32:]
+
+        result.append((seed, pubkey))
+
+    return result
+
+
 def parse_validator_pem(pem_file, index: int = 0):
     pem_file = path.expanduser(pem_file)
     guards.is_file(pem_file)
@@ -94,8 +116,8 @@ def write(pem_file: Union[str, Path], seed: bytes, pubkey: bytes, name: str = ""
     seed_hex = seed.hex()
     pubkey_hex = pubkey.hex()
     combined = seed_hex + pubkey_hex
-    combined = combined.encode()
-    key_base64 = base64.b64encode(combined).decode()
+    combined_bytes = combined.encode()
+    key_base64 = base64.b64encode(combined_bytes).decode()
 
     payload_lines = textwrap.wrap(key_base64, 64)
     payload = "\n".join(payload_lines)
diff --git a/erdpy/wallet/signing.py b/erdpy/wallet/signing.py
index b4e621e5..7f34721b 100644
--- a/erdpy/wallet/signing.py
+++ b/erdpy/wallet/signing.py
@@ -19,6 +19,7 @@ def sign_transaction(transaction: ITransaction, account: IAccount) -> str:
     signed = signing_key.sign(data_json)
     signature = signed.signature
     signature_hex = signature.hex()
+    assert isinstance(signature_hex, str)
 
     return signature_hex
 
@@ -32,3 +33,15 @@ def sign_message_with_bls_key(message, seed):
         return signed_message
     except Exception:
         raise CannotSignMessageWithBLSKey()
+
+
+def sign_message(message: bytes, account: IAccount) -> str:
+    seed: bytes = account.get_seed()
+    signing_key: Any = nacl.signing.SigningKey(seed)
+
+    signed = signing_key.sign(message)
+    signature = signed.signature
+    signature_hex = signature.hex()
+    assert isinstance(signature_hex, str)
+
+    return signature_hex
diff --git a/mypy.ini b/mypy.ini
new file mode 100644
index 00000000..1a8f122f
--- /dev/null
+++ b/mypy.ini
@@ -0,0 +1,16 @@
+[mypy]
+python_version = 3.9
+warn_return_any = True
+warn_unused_configs = True
+
+[mypy-bottle.*]
+ignore_missing_imports = True
+
+[mypy-nacl.*]
+ignore_missing_imports = True
+
+[mypy-debugpy.*]
+ignore_missing_imports = True
+
+[mypy-ledgercomm.*]
+ignore_missing_imports = True
diff --git a/requirements.txt b/requirements.txt
index 6624e8d4..0ce880d1 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -5,5 +5,6 @@ pynacl
 pycryptodomex
 cryptography>=3.2
 prettytable
+ledgercomm
 
 setuptools


Test patch:
diff --git a/erdpy/testnet/config.py b/erdpy/testnet/config.py
index 87171889..15c70e9a 100644
--- a/erdpy/testnet/config.py
+++ b/erdpy/testnet/config.py
@@ -10,7 +10,7 @@
 
 logger = logging.getLogger("testnet")
 
-ConfigurationType = Dict[str, Dict[str, Any]]
+ConfigurationDict = Dict[str, Dict[str, Any]]
 
 
 class Node:
@@ -34,20 +34,33 @@ def __repr__(self) -> str:
 
 
 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 key, path in self.config['folders'].items():
-            if key == "testnet":
+        for folder_key, folder_path in self.config['folders'].items():
+            if folder_key == 'testnet':
                 continue
 
-            path = path.replace('{ELRONDSDK}', str(sdk_folder))
+            folder_path = folder_path.replace('{ELRONDSDK}', str(sdk_folder))
 
-            default_tag = erdpy.config.get_dependency_tag(key)
-            path = path.replace('{TAG}', default_tag)
+            default_tag = erdpy.config.get_dependency_tag(folder_key)
+            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
@@ -55,16 +68,22 @@ def __init__(self, config):
             # 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"):
+            if default_tag.startswith('v'):
                 default_tag = default_tag[1:]
-            path = path.replace('{NOvTAG}', default_tag)
+            folder_path = folder_path.replace('{NOvTAG}', default_tag)
 
-            self.config['folders'][key] = Path(path).expanduser()
+            self.folders[folder_key] = Path(folder_path).expanduser()
 
-        if 'testnet' not in self.config['folders']:
-            self.config['folders']['testnet'] = Path().absolute() / 'testnet'
+        try:
+            self.folders['testnet'] = Path(self.config['folders']['testnet']).expanduser()
+        except KeyError:
+            self.folders['testnet'] = Path().absolute() / 'testnet'
 
-        self.__dict__.update(self.config)
+        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):
@@ -110,13 +129,15 @@ def get_sdk_testnet_config(cls):
         sdk_testnet_config = utils.read_toml_file(filename)
         return merge_configs(default, sdk_testnet_config)
 
-    def node_config_source(self):
+    def node_config_source(self) -> Path:
         return self.node_source() / 'cmd' / 'node' / 'config'
 
-    def node_source(self):
-        return self.folders['elrond_go']
+    def node_source(self) -> Path:
+        path = self.folders['elrond_go']
+        assert isinstance(path, Path)
+        return path
 
-    def proxy_source(self):
+    def proxy_source(self) -> Path:
         return self.folders['elrond_proxy_go']
 
     def proxy_config_source(self):
@@ -268,12 +289,11 @@ def api_addresses_sharded_for_proxy_config(self):
     def proxy_port(self):
         return self.networking["port_proxy"]
 
-    def loglevel(self):
+    def loglevel(self) -> str:
         return self.features.get("loglevel", "")
 
-    def arwen_binary(self):
-        return self.features.get("arwen_binary")
-
+    def arwen_binary(self) -> bool:
+        return self.features.get("arwen_binary", False)
 
     @classmethod
     def default(cls):
@@ -281,7 +301,7 @@ def default(cls):
         config['features'] = {
             'loglevel': '*:DEBUG',
             'proxy': True,
-            'arwen_binary': True
+            'arwen_binary': False
         }
         config['folders'] = {
             'elrond_go':
@@ -318,7 +338,7 @@ def default(cls):
         return config
 
 
-def merge_configs(leftcfg: ConfigurationType, rightcfg: ConfigurationType) -> ConfigurationType:
+def merge_configs(leftcfg: ConfigurationDict, rightcfg: ConfigurationDict) -> ConfigurationDict:
     result = dict()
     result.update(leftcfg)
 
diff --git a/erdpy/testnet/core.py b/erdpy/testnet/core.py
index 36f3600d..d276f7fa 100644
--- a/erdpy/testnet/core.py
+++ b/erdpy/testnet/core.py
@@ -124,5 +124,5 @@ def _is_interesting_logline(logline):
     return any(e in logline for e in ["started committing block", "ERROR", "WARN", "arwen", "smartcontract"])
 
 
-def _dump_interesting_log_line(pid: str, logline: str) -> str:
+def _dump_interesting_log_line(pid: str, logline: str):
     print(f"[PID={pid}]", logline)
diff --git a/erdpy/testnet/genesis_json.py b/erdpy/testnet/genesis_json.py
index 0a34fd09..b8267fbd 100644
--- a/erdpy/testnet/genesis_json.py
+++ b/erdpy/testnet/genesis_json.py
@@ -1,4 +1,3 @@
-
 from typing import Any, List
 
 from erdpy.accounts import Account, Address
diff --git a/erdpy/testnet/node_config_toml.py b/erdpy/testnet/node_config_toml.py
index f0eb46dc..f4ac7153 100644
--- a/erdpy/testnet/node_config_toml.py
+++ b/erdpy/testnet/node_config_toml.py
@@ -1,43 +1,118 @@
-from typing import Any
+from typing import Dict, Any
 
 from erdpy.testnet.config import TestnetConfiguration
 from erdpy.testnet.nodes_setup_json import CHAIN_ID
 
+ConfigDict = Dict[str, Any]
 
-def patch(data: Any, testnet_config: TestnetConfiguration):
-    # Set chain ID and transaction version
-    data['GeneralSettings']['ChainID'] = CHAIN_ID
-    data['GeneralSettings']['MinTransactionVersion'] = 1
 
+def patch_config(data: ConfigDict, testnet_config: TestnetConfiguration):
     data['DbLookupExtensions']['Enabled'] = True
-    data['GeneralSettings']['SCDeployEnableEpoch'] = 0
-    data['GeneralSettings']['BuiltInFunctionsEnableEpoch'] = 0
-    data['GeneralSettings']['RelayedTransactionsEnableEpoch'] = 0
-    data['GeneralSettings']['PenalizedTooMuchGasEnableEpoch'] = 0
-    data['GeneralSettings']['SwitchJailWaitingEnableEpoch'] = 0
-    data['GeneralSettings']['BelowSignedThresholdEnableEpoch'] = 0
-    data['GeneralSettings']['SwitchHysteresisForMinNodesEnableEpoch'] = 1
-    data['GeneralSettings']['TransactionSignedWithTxHashEnableEpoch'] = 0
-    data['GeneralSettings']['MetaProtectionEnableEpoch'] = 0
-    data['GeneralSettings']['AheadOfTimeGasUsageEnableEpoch'] = 0
-    data['GeneralSettings']['GasPriceModifierEnableEpoch'] = 0
-
-    # Make epochs shorter
-    data['EpochStartConfig']['RoundsPerEpoch'] = 100
-
-
-def patch_api(data: Any, testnet_config: TestnetConfiguration):
+
+    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']['ArwenVersions'] = [{'StartEpoch': 0, 'Version': '*'}]
+    virtual_machine['Querying'] = dict()
+    virtual_machine['Querying']['NumConcurrentVMs'] = 1
+    virtual_machine['Querying']['ArwenVersions'] = [{'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
+        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
 
-def patch_system_smart_contracts(data: Any, testnet_config: TestnetConfiguration):
-    data['StakingSystemSCConfig']['StakeEnableEpoch'] = 0
-    data['StakingSystemSCConfig']['StakingV2Epoch'] = 1
-    data['StakingSystemSCConfig']['DoubleKeyProtectionEnableEpoch'] = 0
+    enable_epochs['MaxNodesChangeEnableEpoch'] = [
+        {'EpochEnable': 0, 'MaxNumNodes': 36, 'NodesToShufflePerShard': 4},
+        {'EpochEnable': 1, 'MaxNumNodes': 56, 'NodesToShufflePerShard': 2}
+    ]
+
+    validate_expected_keys(data['EnableEpochs'], enable_epochs)
+    data['EnableEpochs'].update(enable_epochs)
+
+    gas_schedule = dict()
+    gas_schedule['GasScheduleByEpochs'] = [
+        {'StartEpoch': 0, 'FileName': 'gasScheduleV3.toml'}
+    ]
+    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/erdpy/testnet/setup.py b/erdpy/testnet/setup.py
index e4c9b6d3..7f3d080e 100644
--- a/erdpy/testnet/setup.py
+++ b/erdpy/testnet/setup.py
@@ -97,9 +97,7 @@ def create_folders(testnet_config: TestnetConfiguration):
 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)
+        shutil.copytree(config_source, node_config)
 
 
 def copy_validator_keys(testnet_config: TestnetConfiguration):
@@ -113,20 +111,25 @@ def copy_validator_keys(testnet_config: TestnetConfiguration):
 
 def patch_node_config(testnet_config: TestnetConfiguration):
     for node_config in testnet_config.all_nodes_config_folders():
-        config_file = node_config / 'config.toml'
-        data = utils.read_toml_file(config_file)
-        node_config_toml.patch(data, testnet_config)
-        utils.write_toml_file(config_file, data)
+        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)
 
-        config_file = node_config / 'api.toml'
-        data = utils.read_toml_file(config_file)
+        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(config_file, data)
+        utils.write_toml_file(api_config_file, data)
 
-        config_file = node_config / 'systemSmartContractsConfig.toml'
-        data = utils.read_toml_file(config_file)
+        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(config_file, data)
+        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)
@@ -222,20 +225,16 @@ def makefolder(path_where_to_make_folder):
 def patch_source_code(testnet_config: TestnetConfiguration):
     logger.info("Patching the source code...")
 
-    folder = testnet_config.node_source()
-
-    file = path.join(folder, "core/constants.go")
-    content = utils.read_file(file)
-    utils.write_file(file, content)
+    node_source = testnet_config.node_source()
 
-    file = path.join(folder, "cmd/node/main.go")
-    content = utils.read_file(file)
+    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_module_by_key("golang")
+    golang = dependencies.get_golang()
     golang_env = golang.get_env()
     myprocess.run_process(['go', 'env'], env=golang_env)
 
@@ -251,9 +250,9 @@ def build_binaries(testnet_config: TestnetConfiguration):
     logger.info(f"Arwen Binary: {arwen_binary}")
     if arwen_binary:
         logger.info("Building arwen...")
-        node_folder_root = testnet_config.node_source()
         env = dict(golang_env)
-        env["ARWEN_PATH"] = node_folder
+        env["ARWEN_PATH"] = str(node_folder)
+        node_folder_root = testnet_config.node_source()
         myprocess.run_process(['make', 'arwen'], cwd=node_folder_root, env=env)
 
     logger.info("Building proxy...")
@@ -270,7 +269,11 @@ def build_binaries(testnet_config: TestnetConfiguration):
 
     for destination in testnet_config.all_nodes_folders():
         shutil.copy(node_folder / "node", destination)
-        shutil.copy(node_folder / "arwen", destination)
+        if arwen_binary:
+            try:
+                shutil.copy(node_folder / "arwen", destination)
+            except FileNotFoundError:
+                logger.warn("Could not copy the arwen binary!")
 
         if workstation.get_platform() == "osx":
             shutil.copy(libwasmer_path, destination)
@@ -282,7 +285,7 @@ def build_binaries(testnet_config: TestnetConfiguration):
 
 def _get_arwen_version(testnet_config: TestnetConfiguration):
     go_mod = testnet_config.node_source() / "go.mod"
-    lines = utils.read_lines(go_mod)
+    lines = utils.read_lines(str(go_mod))
     line = next(line for line in lines if "github.com/ElrondNetwork/arwen-wasm-vm" in line)
     parts = line.split()
     return parts[1]
diff --git a/erdpy/testnet/wallets.py b/erdpy/testnet/wallets.py
index 0ae5ca69..79c196fa 100644
--- a/erdpy/testnet/wallets.py
+++ b/erdpy/testnet/wallets.py
@@ -50,7 +50,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)
-        result[nickname] = [pubkey, account]
+        result[nickname] = (pubkey, account)
 
     return result
 
diff --git a/erdpy/tests/test_cli_all.sh b/erdpy/tests/test_cli_all.sh
index d791a4b0..bcad751c 100644
--- a/erdpy/tests/test_cli_all.sh
+++ b/erdpy/tests/test_cli_all.sh
@@ -1,14 +1,21 @@
 #!/usr/bin/env bash
 
-source "./shared.sh"
+SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )"
+USE_PROXY=$1
 
 testAll() {
+    pushd $SCRIPT_DIR
+    source ./shared.sh
     source ./test_cli_contracts.sh && testAll
-
     source ./test_cli_wallet.sh && testAll
-    source ./test_cli_validators.sh && testAll
-    source ./test_cli_tx.sh && testAll
-    source ./test_cli_config.sh && testAll
-    source ./test_cli_network.sh && testAll
-    source ./test_cli_cost.sh && testAll
+    source ./test_cli_dns.sh && testAll
+
+    if [ -n "$USE_PROXY" ]; then
+        source ./test_cli_validators.sh && testAll
+        source ./test_cli_tx.sh && testAll
+        source ./test_cli_config.sh && testAll
+        source ./test_cli_network.sh && testAll
+        source ./test_cli_cost.sh && testAll
+    fi
+    popd
 }
diff --git a/erdpy/tests/test_cli_contracts.sh b/erdpy/tests/test_cli_contracts.sh
index 755d90e8..0afa0f79 100755
--- a/erdpy/tests/test_cli_contracts.sh
+++ b/erdpy/tests/test_cli_contracts.sh
@@ -15,8 +15,8 @@ testCreateContracts() {
     ${ERDPY} contract new --template factorial --directory ${SANDBOX} myfactorial-rs || return 1
     ${ERDPY} contract new --template simple-erc20 --directory ${SANDBOX} myerc20-rs || return 1
     ${ERDPY} contract new --template crypto-bubbles --directory ${SANDBOX} mybubbles-rs || return 1
-    ${ERDPY} contract new --template lottery-egld --directory ${SANDBOX} mylottery-rs || return 1
-    ${ERDPY} contract new --template crowdfunding-egld --directory ${SANDBOX} myfunding-rs || return 1
+    ${ERDPY} contract new --template lottery-esdt --directory ${SANDBOX} mylottery-rs || return 1
+    ${ERDPY} contract new --template crowdfunding-esdt --directory ${SANDBOX} myfunding-rs || return 1
 }
 
 testBuildContracts() {
diff --git a/erdpy/tests/test_contracts.py b/erdpy/tests/test_contracts.py
index 3898569c..746c76f3 100644
--- a/erdpy/tests/test_contracts.py
+++ b/erdpy/tests/test_contracts.py
@@ -1,32 +1,42 @@
 import logging
-import unittest
-from pathlib import Path
+import pytest
 
 from Cryptodome.Hash import keccak
 from erdpy.accounts import Account
-from erdpy.contracts import SmartContract
+from erdpy.contracts import SmartContract, _prepare_argument
+from erdpy import errors
 
 logging.basicConfig(level=logging.INFO)
 
 
-class ContractsTestCase(unittest.TestCase):
-    def setUp(self):
-        self.testdata = Path(__file__).parent.joinpath("testdata")
+def test_playground_keccak():
+    hexhash = keccak.new(digest_bits=256).update(b"").hexdigest()
+    assert hexhash == "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"
 
-    def test_playground_keccak(self):
-        hash = keccak.new(digest_bits=256).update(b"").hexdigest()
-        self.assertEqual("c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", hash)
 
-    def test_compute_address(self):
-        contract = SmartContract()
-        contract.owner = Account("93ee6143cdc10ce79f15b2a6c2ad38e9b6021c72a1779051f47154fd54cfbd5e")
+def test_compute_address():
+    contract = SmartContract()
+    contract.owner = Account("93ee6143cdc10ce79f15b2a6c2ad38e9b6021c72a1779051f47154fd54cfbd5e")
 
-        contract.owner.nonce = 0
-        contract.compute_address()
-        self.assertEqual("00000000000000000500bb652200ed1f994200ab6699462cab4b1af7b11ebd5e", contract.address.hex())
-        self.assertEqual("erd1qqqqqqqqqqqqqpgqhdjjyq8dr7v5yq9tv6v5vt9tfvd00vg7h40q6779zn", contract.address.bech32())
+    contract.owner.nonce = 0
+    contract.compute_address()
+    assert contract.address.hex() == "00000000000000000500bb652200ed1f994200ab6699462cab4b1af7b11ebd5e"
+    assert contract.address.bech32() == "erd1qqqqqqqqqqqqqpgqhdjjyq8dr7v5yq9tv6v5vt9tfvd00vg7h40q6779zn"
 
-        contract.owner.nonce = 1
-        contract.compute_address()
-        self.assertEqual("000000000000000005006e4f90488e27342f9a46e1809452c85ee7186566bd5e", contract.address.hex())
-        self.assertEqual("erd1qqqqqqqqqqqqqpgqde8eqjywyu6zlxjxuxqfg5kgtmn3setxh40qen8egy", contract.address.bech32())
+    contract.owner.nonce = 1
+    contract.compute_address()
+    assert contract.address.hex() == "000000000000000005006e4f90488e27342f9a46e1809452c85ee7186566bd5e"
+    assert contract.address.bech32() == "erd1qqqqqqqqqqqqqpgqde8eqjywyu6zlxjxuxqfg5kgtmn3setxh40qen8egy"
+
+
+def test_prepare_argument():
+    assert _prepare_argument('0x5') == '05'
+    assert _prepare_argument('5') == '05'
+    assert _prepare_argument('0x05f') == '005F'
+    assert _prepare_argument('0xaaa') == '0AAA'
+
+    with pytest.raises(errors.UnknownArgumentFormat):
+        _ = _prepare_argument('0x05fq')
+
+    with pytest.raises(errors.UnknownArgumentFormat):
+        _ = _prepare_argument('aaa')
diff --git a/erdpy/tests/test_converters.py b/erdpy/tests/test_converters.py
index 771afe3b..f3edecca 100644
--- a/erdpy/tests/test_converters.py
+++ b/erdpy/tests/test_converters.py
@@ -1,23 +1,19 @@
-import unittest
+from erdpy import utils
 
-from erdpy.conv.conv import Converters
 
+def test_str_to_hex_str():
+    my_str = "1000000"
+    hex_str = utils.str_int_to_hex_str(my_str)
+    assert hex_str == "0f4240"
 
-class ConvertersTestCase(unittest.TestCase):
-    def test_str_to_hex_str(self):
-        my_str = "1000000"
-        hex_str = Converters.str_int_to_hex_str(my_str)
-        self.assertEqual("0f4240", hex_str)
+    my_str = "100000000000000000"
+    hex_str = utils.str_int_to_hex_str(my_str)
+    assert hex_str == "00016345785d8a0000"
 
-        my_str = "100000000000000000"
-        hex_str = Converters.str_int_to_hex_str(my_str)
-        self.assertEqual("00016345785d8a0000", hex_str)
-
-    def test_parse_keys(self):
-        keys = "myKey,newKey,anotherKey"
-        parsed_keys, num_keys = Converters.parse_keys(keys)
-
-        self.assertEqual(3, num_keys)
-        self.assertEqual("@myKey@newKey@anotherKey", parsed_keys)
 
+def test_parse_keys():
+    keys = "myKey,newKey,anotherKey"
+    parsed_keys, num_keys = utils.parse_keys(keys)
 
+    assert num_keys == 3
+    assert parsed_keys == "@myKey@newKey@anotherKey"
diff --git a/erdpy/tests/test_dispatcher_transactions.py b/erdpy/tests/test_dispatcher_transactions.py
index ab39a2c9..3e9c6322 100644
--- a/erdpy/tests/test_dispatcher_transactions.py
+++ b/erdpy/tests/test_dispatcher_transactions.py
@@ -31,7 +31,7 @@ def write_in_queue():
 
 class DispatcherTestCase(unittest.TestCase):
     # this a manual test needs proxy and pem file with funds
-    @unittest.skip
+    @unittest.skip('manual run only')
     def test_multi_thread(self):
         _thread.start_new_thread(write_in_queue, ())
 
@@ -44,12 +44,12 @@ def test_multi_thread(self):
 
         self.assertFalse(False)
 
-    @unittest.skip
+    @unittest.skip('manual run only')
     def test_enqueue_tx(self):
         write_in_queue()
         self.assertFalse(False)
 
-    @unittest.skip
+    @unittest.skip('manual run only')
     def test_dispatcher_txs(self):
         args = TestArgs()
         args.proxy = "http://localhost:7950"
@@ -58,7 +58,7 @@ def test_dispatcher_txs(self):
         queue.dispatch_transactions(args)
         self.assertFalse(False)
 
-    @unittest.skip
+    @unittest.skip('manual run only')
     def test_clean(self):
         queue = TransactionQueue()
         queue.clean_transactions_queue()
diff --git a/erdpy/tests/test_ledger.py b/erdpy/tests/test_ledger.py
new file mode 100644
index 00000000..d194782f
--- /dev/null
+++ b/erdpy/tests/test_ledger.py
@@ -0,0 +1,19 @@
+import unittest
+
+from erdpy.ledger.config import compare_versions
+from erdpy.ledger.ledger_app_handler import get_error
+
+
+class LedgerTestCase(unittest.TestCase):
+    def test_compare_versions(self):
+        self.assertEqual(compare_versions("v1.0.0", "v1.0.1"), -1)
+        self.assertEqual(compare_versions("v1.0.1", "v1.0.1"), 0)
+        self.assertEqual(compare_versions("v1.0.1", "v1.0.0"), 1)
+        self.assertEqual(compare_versions("v1.0.0.1", "v1.0.0"), 1)
+        self.assertEqual(compare_versions("v1.0.1", "v1.0.1.0.0.4"), -1)
+
+    def test_get_error(self):
+        self.assertEqual(get_error(0x6E0C), "invalid fee")
+        self.assertEqual(get_error(0x6E11), "regular signing is deprecated")
+        self.assertEqual(get_error(0x9000), "")
+        self.assertEqual(get_error(0x9999999999), "unknown error code: 0x9999999999")
diff --git a/erdpy/tests/test_testnet.py b/erdpy/tests/test_testnet.py
index 26e98828..08143966 100644
--- a/erdpy/tests/test_testnet.py
+++ b/erdpy/tests/test_testnet.py
@@ -1,6 +1,8 @@
 import os
 import sys
 
+from pathlib import Path
+
 from erdpy import workstation
 from erdpy.testnet import config
 
@@ -59,6 +61,6 @@ def test_init():
     }
 
     testnet_config = config.TestnetConfiguration(data)
-    assert testnet_config.config["folders"]["elrond_go"] == workstation.get_tools_folder() / "bar"
-    assert testnet_config.config["folders"]["elrond_proxy_go"] == workstation.get_tools_folder() / "foobar"
-    assert testnet_config.config["folders"]["testnet"] == "/some/where/mytestnet"
+    assert testnet_config.folders["elrond_go"] == workstation.get_tools_folder() / "bar"
+    assert testnet_config.folders["elrond_proxy_go"] == workstation.get_tools_folder() / "foobar"
+    assert testnet_config.folders["testnet"] == Path("/some/where/mytestnet")