Interacting With Smart Contract On Local Ethereum Blockchain Ethereum Python Solidity

Apr 2nd, 2021 - written by Kimserey with .

In previous posts, we looked into how compilers worked in general and dived into Python in particular looking at how CPython was producing Bytecode which in turn was interpreted by the Python VM. In the world of Ethereum blockchain, smart contracts are the programs that run on the Ethereum VM (EVM). In the same way, smart contracts are compiled into Bytecode which are submitted to the blockchain, and end up being executed on each nodes the decentralized Ethereum blockhain. In today’s post we will be building an example of a smart contract written in Solidity and deploy it in a local Ethereum blockchain.

Local Ethereum Blockchain

In the world of cryptocurrency, Bitcoin and Ether are by far the most known currencies. Ether is the native currency of Ethereum platform. We can see the distinction between Bitcoin and Ethereum where Ethereum isn’t a currency but rather a platform to build more decentralized applications (dapps).

The code written for the dapps are known as smart contracts and are executed on the EVM, a virtual machine living as part of each Ethereum full node.

In each block of the blockchain, the transactions contain the full Bytecode necessary to either create the smart contracts or interact with them. Hence the very first step to take is to run a local blockchain. Ganache-cli is a developer tool from the Trufflesuite which is easy to use and allows us to start a personal Ethereum blockchain for development purposes.

We can install ganache-cli via npm:

1
❯ npm install -g ganache-cli

Then we can just start it with no arguments:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
❯ ganache-cli
Ganache CLI v6.12.2 (ganache-core: 2.13.2)

Available Accounts
==================
(0) 0x148C9803027F6C11d1fA27e4c020D1110DD9d3f0 (100 ETH)
(1) 0x017e779afbdDA9F3f6475106B3847488622A45F8 (100 ETH)
(2) 0xca7611aF4193B93e3F64BCc655fbd33082b1d24e (100 ETH)
(3) 0xa46e614dB2526F833342d8b3C30f93c4D25990A6 (100 ETH)
(4) 0xe61d5842e931708d538C0d68759f4D07656CA1B2 (100 ETH)
(5) 0x10d2C28cbFE65A5EF60a9Acf67c2122BC494C09d (100 ETH)
(6) 0x31F7aB1f3003696C7f8019254b262b2C02403FA5 (100 ETH)
(7) 0xCF5a0569623D87F691dEF1A6f8F61b0670D6BE04 (100 ETH)
(8) 0x865bd4518f8d89cE3eF664bA6EcFd63EA3a24E3b (100 ETH)
(9) 0xBE35Cf10aCE9474a00204138149E38605723bff5 (100 ETH)

Private Keys
==================
(0) 0x0fd133db479ab2a59bef8e18dd05dfddfe4ee71c327f79b9649faf418f23d435
(1) 0x42e41534f08a1d8006568562fb9dff8eac489129fe7839e1609573a54c179b54
(2) 0x320a99dad2872577bb90967ca9e7bc6232397434627d9f00b4dda4f1ce0f9df2
(3) 0x8515841f9e19ac2573c6c6336f503bdd067f19f1aba394102fd5379fdb3c40c8
(4) 0x5f4df4d0c8c2160bed0663e3f961f2d08366428b115bc8e51b3e65d3781d6e4f
(5) 0x9892944e28a0659f04d59edbdb73ea53f5030f8ee53f4deca31862b835fbde91
(6) 0x8ca4fcf90f1d4d2bd6dc2554a26eee9b092b73e3b7b6c47f7975a19612445f9d
(7) 0x52354d3a3c739c59a32ede6a1dd84b539ba03159b2ce606d4a73c9422e808139
(8) 0xb3486ac1c05333c794dd01f04fc3691b5dc696d72fe20bd4f53e67ba05ccb4ff
(9) 0xe31dc87f86f88b8951ce42a138656ffe071e2f6eb02a83062fc5e4682d675c9d

HD Wallet
==================
Mnemonic:      royal correct three produce sibling hip armor custom vault neglect sample unusual
Base HD Path:  m/44'/60'/0'/0/{account_index}

Gas Price
==================
20000000000

Gas Limit
==================
6721975

Call Gas Limit
==================
9007199254740991

Listening on 127.0.0.1:8545

Ganache-cli starts a blockchain with 10 accounts with 100 ETH each and provide private keys to those accounts.

If we want to continue from a deterministic state, we can keep the mnemonic of the HD wallet, and store the data via --db param:

1
❯ ganache-cli -d -m "royal correct three produce sibling hip armor custom vault neglect sample unusual" --db="./data/save/" -d

We now have blockchain running locally.

Solidity Smart Contract

Now that we have the local blockchain running, we can start writing our first Solidity contract. Solidity is an object oriented language for writiing smart contracts.

We can then create our first contract SimpleStorage.solc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// SPDX-License-Identifier: GPL-3.0
pragma solidity ^0.8.0;

contract SimpleStorage {
    uint256 storedData;

    function set(uint256 x) public {
        storedData = x;
    }

    function get() public view returns (uint256) {
        return storedData;
    }
}

This program contains two functions, a get function allowing us to retrieve a value, and a set function to set the value.

Then install solc the Solidity compiler via npm:

1
❯ npm install -g solc

We can then manually verify that we are able to compile the Solidity contract using the compiler:

1
2
3
4
5
❯ solc --bin SimpleStorage.sol

======= SimpleStorage.sol:SimpleStorage =======
Binary:
608060405234801561001057600080fd5b5061012f806100206000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c806360fe47b11460375780636d4ce63c14604f575b600080fd5b604d600480360381019060499190608f565b6069565b005b60556073565b6040516060919060c2565b60405180910390f35b8060008190555050565b60008054905090565b60008135905060898160e5565b92915050565b60006020828403121560a057600080fd5b600060ac84828501607c565b91505092915050565b60bc8160db565b82525050565b600060208201905060d5600083018460b5565b92915050565b6000819050919050565b60ec8160db565b811460f657600080fd5b5056fea264697066735822122098d1fca8abf99722510c1ed93797311390db355fbd30d6e885a2b2a87113539564736f6c63430008000033

We now have a smart contract ready to be deployed on the blockchain.

Interacting with Ethereum with Web3

So far we’ve setup a local blockchain and built a smart contract. The last piece is for us to be able to publish the smart contract to the blockchain, and then interact with it.

The interaction is managed from a collection of libraries known as Web3. Web3 has multiple implementations, in this post we’ll look at Web3.py, the Python implementation.

For Python, we can get web3 via pip:

1
❯ pip install web3

and get the wrapper around solc via py-solc:

1
❯ pip install py-solc

Having those two installed, we can then start by importing and initialising web3 given the localhost HTTP address of our local blockchain started by ganache-cli:

1
2
3
4
5
import json
from web3 import Web3
from solc import compile_standard

w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))

The first step we have to do is to compile our Solidity contract we created previously. This can be done using compile_standard function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
def compile_contract():
    """Compile solidity smart contract.

    Returns:
        (abi, bytecode): abi used to interact with the contract, bytecode for the EVM.
    """
    # Get Smart Contract source code and compile Smart Contract
    with open("SimpleStorage.sol", "r") as f:
        compiled_sol = compile_standard(
            {
                "language": "Solidity",
                "sources": {"SimpleStorage.sol": {"content": f.read()}},
                "settings": {
                    "outputSelection": {
                        "*": {
                            "*": ["metadata", "evm.bytecode", "evm.bytecode.sourceMap"]
                        }
                    }
                },
            }
        )

    bytecode = compiled_sol["contracts"]["SimpleStorage.sol"]["SimpleStorage"]["evm"][
        "bytecode"
    ]["object"]
    abi = json.loads(
        compiled_sol["contracts"]["SimpleStorage.sol"]["SimpleStorage"]["metadata"]
    )["output"]["abi"]
    return (abi, bytecode)

compile_standard takes a JSON input which sepcifies what to compile, which language, and the output that we want to get out of the compilation. Here we specify that we want the metadata, bytecode and sourceMap. From the output, we then extract the bytecode and the abi.

1
(abi, bytecode) = compile_contract()

The ABI is the application binary interface which is the interface used for web3 to allow us to interact programatically with the contract. For example if we were to print the abi we will see the definition of our get and set:

1
2
3
4
5
6
7
8
9
10
11
12
In [4]: abi
Out[4]: 
[{'inputs': [],
  'name': 'get',
  'outputs': [{'internalType': 'uint256', 'name': '', 'type': 'uint256'}],
  'stateMutability': 'view',
  'type': 'function'},
 {'inputs': [{'internalType': 'uint256', 'name': 'x', 'type': 'uint256'}],
  'name': 'set',
  'outputs': [],
  'stateMutability': 'nonpayable',
  'type': 'function'}]

In contrast the bytecode will be the data sent in the transaction to deploy our contract. The bytecode is the code that the EVM understands and is not human readable:

1
2
In [5]: bytecode
Out[5]: '608060405234801561001057600080fd5b5061012f806100206000396000f3fe6080604052348015600f57600080fd5b506004361060325760003560e01c806360fe47b11460375780636d4ce63c14604f575b600080fd5b604d600480360381019060499190608f565b6069565b005b60556073565b6040516060919060c2565b60405180910390f35b8060008190555050565b60008054905090565b60008135905060898160e5565b92915050565b60006020828403121560a057600080fd5b600060ac84828501607c565b91505092915050565b60bc8160db565b82525050565b600060208201905060d5600083018460b5565b92915050565b6000819050919050565b60ec8160db565b811460f657600080fd5b5056fea264697066735822122098d1fca8abf99722510c1ed93797311390db355fbd30d6e885a2b2a87113539564736f6c63430008000033'

The next step will be to deploy the contract to the blockchain:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def deploy_contract(abi, bytecode):
    """Deploy the contract to the blockchain.

    Returns:
        Address of the contract.
    """
    tx_hash = (
        w3.eth.contract(abi=abi, bytecode=bytecode)
        .constructor()
        .transact(
            {
                "from": w3.eth.accounts[0]
            }
        )
    )
    tx_receipt = w3.eth.getTransactionReceipt(tx_hash)
    print("Deploy Contract Transaction:\n", w3.eth.getTransactionReceipt(tx_hash), "\n")
    return tx_receipt.contractAddress

In order to deploy the contract, we create an instance of the contract with w3.eth.contract(abi=abi, bytecode=bytecode).constructor() and call transact to send a transaction to the blockchain.

1
2
(abi, bytecode) = compile_contract()
contract_address = deploy_contract(abi, bytecode)

Once we deploy the contract, we can see on ganache-cli client the transaction being instantly mined:

1
2
3
4
5
6
7
8
9
10
11
12
Listening on 127.0.0.1:8545
eth_accounts
eth_estimateGas
eth_blockNumber
eth_getBlockByNumber
eth_sendTransaction

  Transaction: 0x68347fbcc626079a215c336de0d1929627c4cb62a5011398d6a32ed6503a7a02
  Contract created: 0x504a37b2f6be939972723fd5176a7dd6a03fc73d
  Gas usage: 118807
  Block Number: 1
  Block Time: Tue Jan 19 2021 10:27:52 GMT+0000 (Greenwich Mean Time)

We see that creating the contract used 118807 gas. Gas is the fee that one must pay in order to execute Bytecode on the EVM in the blockchain. The price of the gas are denoted in Gwei (giga wei) which is equivalent to 0.000000001 ETH (10-9 ETH).

Now that the contract has been submitted on the blockchain, we can now interact with it:

1
2
3
4
5
6
7
def get_stored_data(abi, contract_address):
    """Get the data stored in smart contract.

    A .call is a local query to the blockchain, it does not consume any gas (view, pure, constant).
    """
    return w3.eth.contract(address=contract_address, abi=abi).functions.get().call()
    

In order to use our contract, we must use the contract address which we saved from deploy_contract. We can always get it back by inspecting the block which was mined and then inspect the transaction:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
In [5]: w3.eth.get_block(1)
Out[5]: 
AttributeDict({'number': 1,
 'hash': HexBytes('0x5aad1330634bf03ddfbed5763c788bca68fb6437dd91c46436eebc1179c11a5c'),
 'parentHash': HexBytes('0x043e6f99ccf9a1bfdd50cba33fdecbcce1f64dee162b78d46f7a444d8a58d6f4'),
 'mixHash': HexBytes('0x0000000000000000000000000000000000000000000000000000000000000000'),
 'nonce': HexBytes('0x0000000000000000'),
 'sha3Uncles': HexBytes('0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347'),
 'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
 'transactionsRoot': HexBytes('0x9808237c9c387e54481a9879dc6944c222e84bc4b78b59effc9758f295de2754'),
 'stateRoot': HexBytes('0x7897049ea83a93a4e56b5c5a17061e37d87503204127e2febf8a82f552fb0644'),
 'receiptsRoot': HexBytes('0xd5f842b1e60aefca88bef659c93cd1ecb3ddce163e5815871404e184551f0d45'),
 'miner': '0x0000000000000000000000000000000000000000',
 'difficulty': 0,
 'totalDifficulty': 0,
 'extraData': HexBytes('0x'),
 'size': 1000,
 'gasLimit': 6721975,
 'gasUsed': 118807,
 'timestamp': 1611052072,
 'transactions': [HexBytes('0x68347fbcc626079a215c336de0d1929627c4cb62a5011398d6a32ed6503a7a02')],
 'uncles': []})

Then for example:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
In [7]: w3.eth.getTransactionReceipt(0x68347fbcc626079a215c336de0d1929627c4cb62a5011398d6a32ed6503a7a02)
Out[7]: 
AttributeDict({'transactionHash': HexBytes('0x68347fbcc626079a215c336de0d1929627c4cb62a5011398d6a32ed6503a7a02'),
 'transactionIndex': 0,
 'blockHash': HexBytes('0x5aad1330634bf03ddfbed5763c788bca68fb6437dd91c46436eebc1179c11a5c'),
 'blockNumber': 1,
 'from': '0xa829268Be566a267B1774d1C7C5c530C94aFE835',
 'to': None,
 'gasUsed': 118807,
 'cumulativeGasUsed': 118807,
 'contractAddress': '0x504A37B2f6be939972723Fd5176a7Dd6A03FC73D',
 'logs': [],
 'status': 1,
 'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000')})

Here we have the contract address 0x504A37B2f6be939972723Fd5176a7Dd6A03FC73D.

Then with the contract, we start first by calling the get, we can access our contract functions via .functions.myfunct() and because this is a view hence doesn’t mutate the state, we use .call() which would directly inspect the latest state of the blockchain.

1
2
3
(abi, bytecode) = compile_contract()
contract_address = deploy_contract(abi, bytecode)
get_stored_data(abi, contract_address)

When we make the call we see that it doesn’t create any transaction:

1
eth_call

This is because calls are free as they just read the latest state precomputed by the Ethereum node.

In contrast, when we call set, we need to do it via transact:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def set_stored_data(abi: dict, contract_address: str, from_address: str, data: int):
    """Set the data stored in smart contract.

    A .transact issues a transaction to the blockchain and consume gas.
    """
    tx_hash = (
        w3.eth.contract(address=contract_address, abi=abi)
        .functions.set(data)
        .transact(
            {
                "from": from_address
            }
        )
    )
    print("Set Data Transaction:\n", w3.eth.getTransactionReceipt(tx_hash), "\n")

We then see that transact just like submitting the contract is mined instantly on ganache-cli.

1
2
3
(abi, bytecode) = compile_contract()
contract_address = deploy_contract(abi, bytecode)
set_stored_data(abi, contract_address, w3.eth.accounts[2], 21)

And when we look at the blockchain client:

1
2
3
4
5
6
eth_sendTransaction

  Transaction: 0xda6260c174946e61f92711735ec91363d11dce5e1abc343a7c3a3053d07877a0
  Gas usage: 41602
  Block Number: 2
  Block Time: Tue Jan 19 2021 10:27:52 GMT+0000 (Greenwich Mean Time)

We see that it has consumed 41602 which is the price fee to run the set function on the contract.

We were now able to run a local blockchain, create a smart contract, deploy the contract and interact with it! And that concludes today’s post!

Conclusion

In today’s post we looked into Ethereum EVM. We started by running a local blockchain. We then moved on to writing our first smart contract in Solidity and lastly we looked into deploying and running our smart contract on the blockchain via Web3. Hope you liked this post and I see you on the next one!

External Sources

Designed, built and maintained by Kimserey Lam.