NEWS
๐ JOIN THE SOLITUDE DEV COMMUNITY ON DISCORD for:
USEFUL LINKS
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:
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:
solitude.yaml
Project.SourceDir
field of the solitude.yaml
Client.Endpoint
in the solitude.yaml
Client.Endpoint
returns trace info (frames, stack, memory and storage)Tools.EthLint.Version
) against the contracts in Project.SourceDir
.Tools.GanacheCli.Version
at Server.Host
: Server.Port
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.
_
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
_
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 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.
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);
}
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 ๐ผ
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.
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:
For more on how to use the solitude debug you can have a look on solitude_examples/e04_cat_shelter
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:
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:
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:
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