-->


๐Ÿ Pythonic framework to deploy, interact, test and debug your Solidity contracts


No migrations are allowed in the sour realm of Solitude

Star


Getting started with no pain


๐Ÿ’ฟ Install Solitude

We recommend to use a virtual environment for installing solitude from its repository.

python3 -m venv myenv
source myenv/bin/activate
pip install git+https://github.com/incerto-crypto/solitude.git@develop

๐ŸชŸ On Windows make sure you have installed:

๐ŸชŸ Make sure you have also installed:


๐Ÿ†• Create a new project

Solitude comes with an handy CLI.

Running solitude init in your project folder will create a default solitude.yaml configuration file.

mkdir -p path/to/MyProject
cd path/to/MyProject
solitude init

๐Ÿ’ป Other CLI commands are:

  • install: install all the required tools configured in the solitude.yaml
  • compile: compile all the smart contracts in the Project.SourceDir field of the solitude.yaml
  • debug: given a transactionHash starts an interactive gdb like debugger against the the ethereum node at Client.Endpoint in the solitude.yaml
  • trace: given a valid transaction hash in the etehreum node reachable at Client.Endpoint returns trace info (frames, stack, memory and storage)
  • lint: runs the solidity linter (for example EthLint Version Tools.EthLint.Version) against the contracts in Project.SourceDir.
  • server: runs a Ganache version: Tools.GanacheCli.Version at Server.Host : Server.Port

๐Ÿ†• Run an existing project

Usually to run the first time a solitude project you need to install the required tools and compile the contracts from the Project.SourceDir into the Project.ObjectDir directory.

cd path/to/MyProject
solitude install
solitude compile

You need to install the required tools and to compile your contracts everytime you update them. For example the solitude_examples/e01_cat_shelter in the example respository defines the congifuration file as follows:

- Project.SourceDir: .
- Project.ObjectDir: ./build
- Tools.Directory: ~/.solitude-dev
- Tools.Required:
  - Solc
  - GanacheCli
- Tools.Solc.Version: 0.5.2
- Tools.GanacheCli.Version: 6.4.1

Running solitude compile the tool will make sure Solc and GanacheCli of the desired versions are installed in ~/.solitude-dev.

Running solitude install it will compile all the contracts (*.sol) in current directory . (in this case only CatShelter.sol) creating the object json in the project sub folder build.

Finally the example shows hot to write tests in python for checking the corretness of the contract. We explain how the solitude framework can be integrated with python testing tools in the Testing with Solitude section.


Solitude.yaml

A solitude.yaml configuration file looks as simple as this ๐Ÿ‘€

Projects variables are related with a specific project and their values should be unique to the project
  • ๐Ÿ†• Name is the name of your project
  • ๐Ÿ“ SourceDir is where your contracts source code is stored
  • ๐Ÿ—„๏ธ ObjectDir is where your compiled contracts source code are going to be stored
Tools variables are about the tools you need to be up and running.
  • โš™๏ธ Required defines the tools you need to have installed in ๐Ÿ“ Directory.
  • ๐Ÿ”ข Versions of the required tools can be specified in the configuration (e.g Solc.Version).
Running solitude install you can make sure the required tools are installed in the tool directory. Different solitude projects can share the same tool directory.

    _
    Project.Name: MyProject
    Project.SourceDir: ./contracts
    Project.ObjectDir: ./build/contracts
    Tools.Directory: ~/.solitude-dev
    Tools.Solc.Version: 0.5.2
    Tools.GanacheCli.Version: 6.4.1
    Tools.EthLint.Version: 1.2.4
    Tools.Required:
        - Solc
        - GanacheCli
    Server.Port: 8545
    Server.Host: 127.0.0.1
    Server.GasPrice: 20000000000
    Server.GasLimit: 6721975
    Server.Accounts:
        - 0xedf206987be3a32111f16c0807c9055e2b8b8fc84f42768015cb7f8471137890, 200 eth
        - 0x0ca1573d73a070cfa5c48ddaf000b9480e94805f96a79ffa2d5bc6cc3288a92d, 100 eth
    Client.Endpoint: http://127.0.0.1:8545
    Client.GasPrice: 20000000000
    Client.GasLimit: 6721975
    _


Solitude client

The Solitude client object allows to communicate with an ethereum node client using web3 and rpc. It is mainly used to produce contract objects and assist deployment on a blockchain instance. For each created (or referred) contract, it stores ABI and optionally the bytecode.

Accounts are also managed by the client object by pushing a context. Contexts can be nested and the account on the top of the context stack will be used.

Deploy a contract is as easy as:

with client.account(client.address(0)):
    client.deploy("ContractName", args=())

In the solitude.yaml configuration you can specify how to reach the ethreum client. For example in a testing context you might want to connect Ganache runs on the localhost at the 8545 port)
You can also connect to a geth node however some of the clients configurations will not work.


Solitude server

Solitude clients allow you to perform some actions on a ethereum node. In the config file you can specify how the client can reach the ethreum node (acting as a server) in the Client.Endpoint. You can also spin a testing ethereum server typing solitude server. It will run a Ganache node at the address specified in Server.Host and Server.Port of your solitude.yaml. If your client is pointing to an already running etehreum node you can omit this configuration section. In many cases we want the solitude client to point to the same address we have run the solitude server.


Dynamic creation of Solitude components

You can dinamically create solitude servers and clients. In the solitude_examples/e02_cat_rescue, in the main function of the cat_rescue.py a solitude factory is instantiated from a config file. The factory then creates and starts a server and stores its endpoint. Then two clients are created and made them point to previously created server. They both import the same compiled contracts (factory.get_objectlist()) using the update_contracts method.

The first client, acting as the account 0 from the Server.Accounts list, deploys the CatShelter contract on the server and runs the rescue_cats on a thread.

The rescue_cats method adds a new adoptable cat to the shelter every two seconds by making a rescue transaction.

The second client acting as the account 1 runs on thread the adopt_cat method which adds a filter to Rescued events.

The rescue transaction in the smart contract before finishing emits a Rescued event with the id of the rescued cat. The second client gets the event, retrieves its id and adopts the id-th cat making the adopt transaction.

    function rescue() public
    {
        require(msg.sender == owner);
        adopters.push(address(0));
        emit Rescued(adopters.length - 1);
    }


Testing with Solitude

Using solitude you can test your contracts with pytest. Solitude has already defined common context that can be used as test fixtures and stubs for triggering errors.

The following contract tracks the ownership of the i-th cat in the adopters array, by assigning the ownerโ€™s address to the i-th position of the array. You can find all the code in the solitude example repository in the solitude_examples/e01_cat_shelter folder.

pragma solidity ^0.5.2;

contract CatShelter
{
    address[16] public adopters;
    function adopt(uint256 i) public
    {
        require(i < adopters.length);
        require(
            adopters[i] == address(0),
            "Cat has already been adopted");
        adopters[i] = msg.sender;
    }

We can easily test the adopt function with a python test. Normally for each new test we would need to stop the current ethereum server node and run a clean one. Solitude can manage the spin and tear down of an new ethereum node for each test by setting the Testing.RunServer in the configuation. The server testing instances will be run in a port range specified in the config file, for example: Testing.PortRange: [8600, 8700]

import pytest
from solitude.testing import sol

def test_001_adopt_cat(sol, shelter, account0):
   
    CAT_INDEX = 3
    with sol.account(sol.address(0)):
        contract = sol.deploy("CatShelter", args=())
        contract.transact_sync("adopt", CAT_INDEX)

    assert sol.address(0) == contract.call("adopters", CAT_INDEX)

The sol object pushes a default context configured with the options in the solitude.yaml. On top of the default sol context we can push a more detailed context.

For example in this test we are defining the default account to the 0-th from the available ones in the Client. You can define the available accounts and their balance for the server test in the Solitude.Accounts array in the config.

Looking the solitude.yaml at the accounts section:

 Solitude.Accounts:
    - 0xedf206987be3a32111f16c0807c9055e2b8b8fc84f42768015cb7f8471137890, 200 eth
    - 0x0ca1573d73a070cfa5c48ddaf000b9480e94805f96a79ffa2d5bc6cc3288a92d, 100 eth

with sol.account(sol.address(0)): is setting 0xedf206987...1137890 as the account that deploys the CatShelter contract and calls after the adopt method.

Notice we are deploying and transacting within the same context, so make sure the selected account has got enough funds.

Finally we check that our default address is the owner of the third cat.

Congratulations you now own a cat on the blockchain ๐Ÿ˜ผ


Debug Tests in Solitude

An handy way to debug your tests is using vscide. Installing the python extension we can easily create a launch configuration for the pytest module. This is how the launch.json configuration looks like in the .vscide folder.

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Python: Module",
            "type": "python",
            "request": "launch",
            "module": "pytest",
            "pythonPath": "/path/to/python/environment/where/solitude/installed/python3"
        }
    ]
}

Opening the solitude_examples/e01_cat_shelter as workspace we can set a few breakpoints to get more familiar with the solitude framework.

In the Call stack panel we can see two threads one running our tests and one running the ganache node ( aka solitude server). For each test, solitude restores the default state of the ethereum node as per the config file.

In the variable section we can see our variables and we can also inspect the sol object which manages the testing context. The sol object contains an instance of solitude server (class solitude.server.eth_test_server.ETHTestServer) which manages the server process and an instance of solitude client (classsolitude.client.eth_client.ETHClient) which allows to invoke commands against the node. The solitude ETHClient class contains a web3 client which points to the solitude server. we can use it in our test to get meaningful information. For example in the previous screen in the Watch panel we were able to see the balance of the 0xedf206987...1137890 account (i.e. account0) going down after the deplyoment and after the transaction. We can see it accessing the web3 client in the sol object: sol.client.web3.eth.getBalance(account0). If wee need we can use these information in our tests as well.

Debug Transactions in Solitude with sold

In the last example we had a debugging session which had running an ethereum node server (ganache) and a pytest session. One interesting information we can get during the debuggin session is to get the transaction id (txhash) of a transaction and debug itself inside a smart contract. In the test we save in the transaction object the result of the transact_sync on the adopt contract method (line 24). Decondig the txhash member of the transaction object in the Watch panel by using binascii.hexlify(transaction.txhash).decode() we can start a solitude debuggin session on that specific transaction, in this case โ€˜bceff320c06548233e9c4b1d464ac528124fd9ccf0a9425aa8f6f49798704170โ€™.

Letโ€™s open a terminal in the same folder and type solitude debug 0xbceff320c06548233e9c4b1d464ac528124fd9ccf0a9425aa8f6f49798704170. Notice we have added the 0x at the beginning.

โญ๏ธ The sold command line will be launch and we would be able to debug the transaction that has happened by using command line:

  • backtrace: prints the call stack and function parameter
  • break function_name : set a breakpoint to the function function_name
  • continue: continue execution until it hits a breakpoint or the end of the transaction instructions
  • info locals: prints the local memory variable and their value
  • next : goes to next instruction
  • print variable_name: prints a memory variable value with the name variable_name

For more on how to use the solitude debug you can have a look on solitude_examples/e04_cat_shelter

Debug Transactions in Solitude with the VSCode Extension

Note: For now only Linux and WLS are supported

You can also debug a transaction using the vscode solitude extension.

Letโ€™s install the Solitude Debug extension from the vscode marketplace or from the website.

Letโ€™s clone the Solitude Examples repository and make sure we have Solitude and its requirements installed:

Requirements on your OS:

  • Python3.5 or greater
  • node8 or greater
  • yarn / npm

Create a python3 virtual environment and activate it. Install solitude.

python3 -mvenv myenv
source myenv/bin/activate
pip install git+https://github.com/incerto-crypto/solitude.git

Letโ€™s open the solitude_examples/e04_cat_shelter folder. It is important that the solitude.yaml is in the visual code workspace root or you might need to change the solitudeConfigPath field in the .vscode/lauch.json

Remember to run solitude install and solitude compile to make sure you have the needed tools and the contracts compiled in the right folder as per the project README.

We need now to tell vscode the python and solitude executable path. We will use the one we have just created along with the new python virtual environment.

Open the VSCODE settings by pressing CTRL/CMD + SHIFT + p and select Preferences: Open Settings UI. In the search bar letโ€™s type Solitude and letโ€™s point the two fields to our executables. If we created the virtual environemtn in the dev folder the setting needs to look like it:

We navigate to the Debug vscode panel and we check we have a .vscode/launch.json file and a solitude configuration in it.

Before starting a debugging session we run the example script :

python token_launch.py

Finally we run the debugger pressing the green arrow Transaction Hash Id on the top left corner or pressing F5.

Visual code should prompt us with all the possible transaction we want to debug.

And we can now debug our transaction:

Alt Text


A little bit more about the script we are going to debug:

The contracts folder contain MyToken.sol which is an instance of an ERC20

pragma solidity ^0.5.2;

import "./openzeppelin-solidity/contracts/token/ERC20/IERC20.sol";
import "./openzeppelin-solidity/contracts/token/ERC20/ERC20.sol";
import "./openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol";
import "./openzeppelin-solidity/contracts/token/ERC20/ERC20Detailed.sol";

contract MyToken is ERC20, ERC20Mintable, ERC20Detailed
{

  constructor(string memory _name, string memory _symbol, uint8 _decimals)
    ERC20Detailed(_name, _symbol, _decimals)
  public {

  }
}

From the python environment where we have installed solitude letโ€™s run the token_launch.py. The script loads a config file and uses its options to run a server and create a client. The client gets the reference of the compiled contracts client by calling update_contracts(factory.get_objectlist())` .

The Project.ObjectDir field in the solitude.yaml represents the path where the compiled contracts are stored after the compilation. The repository instructions asks the user to invoke solitude compile to make sure the compiled contracts are available for this script to get the references.

client = factory.create_client()
client.update_contracts(factory.get_objectlist())
owner = client.address(0)
george = client.address(1)

The script assigns aliases to address. The address at position 0 in the config Server.Accounts array is assigned to the alias owner and the address at position 1 to george. The owner deploys the MyToken contract and mint 1000 tokens.

function mint(address to, uint256 value) public onlyMinter returns (bool) {
    _mint(to, value);
    return true;
}

Note that the mint transaction method is inherited from the ERC20Mintable interface and can be run only from the contractโ€™s owner because of the onlyMinter modifier. This method creates new tokens and assign it to the owner of the contracts. After minting and getting the token, the owner account transfers 100 tokens to george. We can modify the token_launch.py and change the transfered amount from 100 tokens to 10000.

txinfo = token.transact_sync("transfer", george, 10000)

If we launch it again after minting and getting the token, the owner account tries to transfer 1000*10 tokens to georgeโ€™s account, however he has only 1000 and the transaction fails. We can now debug and see the exception in visual code.

with client.account(owner):
    token = client.deploy("MyToken", args=("Token", "TKN", 0))
    txinfo = token.transact_sync("mint", owner, 1000)
    print_txinfo(txinfo)
    txinfo = token.transact_sync("transfer", george, 10000)
    print_txinfo(txinfo)

The script will execute three transactions:

  • Contract deployment (ignored by this exstension): Successful
  • Mint: Successful
  • Transfer: Failed (not enough tokens)

Running the script we have the following output with the two (mint, transfer) transaction ids which are the same suggested by the extension.

python token_launch.py

TX FROM 0x29c48A8FDeDc65cBe4019557de677bA48563f76d
   TO MyToken@0x749D167BB1A3FFdD6C21C8B75fa6958Bf327D51D
   FUNCTION mint('0x29c48A8FDeDc65cBe4019557de677bA48563f76d', 1000)
   TXHASH 0x29effbc316d8fa7bab0ea97368aafc3d15db95e4e6b0e4b217e33e77d2136ada

solitude.common.errors.TransactionError:transfer Transaction returned status 0. txhash: 0xd63db6285e44a79d0b6532b9e18490b8a8c704672f45189ac79bc33f6feb5d19