Les contrats intelligents (en anglais Smart Contracts) sont des protocoles informatiques qui facilitent, vérifient et exécutent la négociation ou l’exécution d’un contrat, ou qui rendent une clause contractuelle inutile (car rattachée au contrat intelligent). Les contrats intelligents ont généralement une interface utilisateur et émulent la logique des clauses contractuelles.

Les partisans des contrats intelligents affirment que de nombreux types de clauses contractuelles peuvent ainsi être partiellement ou totalement auto-exécutées ou exécutées à la validation ou les deux.

Les contrats intelligents visent à assurer une sécurité supérieure à la mise en application de la loi sur les contrats et de réduire les coûts de transaction associés à la passation des contrats. [Source: Wikipedia]

Concrètement, comment communiquer avec un Smart Contract ?

Dans ce tutoriel, on va créer, déployer et interagir avec un Ethereum Smart Contract en utilisant Python3 et les outils de développement d’un Smart contract d’Ethereum, qu’on va les voir tout de suite, sans la création d’un Noeud (Geth ou/et Parity), mais seulement en utilisant les noeuds de infura.io.

prérequis:

Création d’un environnement de travail:

Création d’un environnement virtuel pour Python3 et création d’un dossier dans l’environnement virtuel avec des fichiers qu’on va utiliser pour la compilation, déploiement, interaction et tester la communication avec un Smart Contract.

NB: Le code qui va suivre est testé sur Ubuntu 16.04.3 et avec Python 3.5.2

$> cd # aller au dossier /home/user/
$> virtualenv eth_contract # création d'un environnement virtuel 
$> cd eth_contract/bin # entrer dans le dossier bin
$> source activate # activer l'environnement virtuel
$> cd .. # retour à la racine
$> mkdir working_with_contracts # créer un dossier
$> cd working_with_contracts # enter dans le dossier
$> touch contract.sol # fichier contenant le code du Smart contract
$> touch compile_contract.py # fichier contenant le code qui va compiler le Smart Contract
$> touch interact_contract.py # fichier contenant le code qui va interagir avec le Smart Contract
$> touch test_compile.py # tester la compilation du Smart Contract 
$> touch test_deploy.py # tester le déploiement du Smart contract
$> touch test_interact.py # tester l'interaction avec le Smart Contract

Allons, codons un peu .. :

contract.sol:

pragma solidity ^0.4.18;

contract Greeter {
    string public greeting;

    function Greeter() public {
        greeting = "Hello";
    }

    function setGreeting(string _greeting) public {
        greeting = _greeting;
    }

    function greet() view public returns (string) {
        return greeting;
    }
}

compile_contract.py:

# Compile contract
# Author: Chiheb Nexus - 2018
# License: GPLv3
#


import json
from solc import compile_source

class CompileContract:
    '''Compilation d'un Smart contract'''
    def __init__(self, path_file, contract_name):
        self.path_file = path_file
        self.contract_name = contract_name

    def read(self, verbose=False):
        '''Lecture du fichier contenant le code source du Smart Contract'''
        if verbose:
            print('-> Lecture du fichier: {0}'.format(self.path_file))
        with open(self.path_file, 'r') as f:
            data = f.read()

        print('-> Lecture terminée! du fichier: {0}'.format(self.path_file))
        return data

    def write(self, file_name, data, verbose=False):
        '''Écriture du output de la compilation du Smart contract'''
        if verbose:
            print('-> Écriture du contract compilé dans le fichier: {0}'.format(file_name))

        with open(file_name, 'w') as f:
            f.write(json.dumps(data['<stdin>:{0}'.format(self.contract_name)]))

        if verbose:
            print('-> Écriture terminée avec succès! dans le fichier: {0}'.format(file_name))
        
    def compile(self, verbose=False):
        '''Compiler le code du Smart Contract'''
        if verbose:
            print('-> Compilation en cours ...')

        compiled_sol = compile_source(self.read(verbose=True))
        self.write('compiled_contract', compiled_sol, verbose=True)

        if verbose:
            print('-> Compilation terminée avec succès!')

interact_contract.py:

# Déploiement et interaction avec un Smart contract
# Author: Chiheb Nexus - 2018
# License: GPLv3
#

import json
import time
from web3 import Web3

class InteractWithContract:
    '''Interaction avec un Smart Contract'''
    def __init__(self, compiled_sol_path=None, public_key=None, private_key=None, provider):
        # Dans cet exemple on va utiliser infura.io comme provider
        # Et la communication avec infura peut se faire en utilisant le protocole HTTP
        # Ou WS (Web Socket)
        self.w3 = Web3(Web3.HTTPProvider(provider))
        self.compiled = compiled_sol_path
        # ABI et le BIN du output compilé du Smart Contract
        self.ABI, self.BIN = '', ''
        # L'adresse publique et privée de celui qui va déployé et communiquer avec le Smart Contract
        self.pub, self.priv = public_key, private_key


    def read(self, verbose=False):
        '''Lecture du ouput compilé et extraction de ABI et du BIN'''
        if verbose:
            print('-> Lecture du fichier: {0}'.format(self.compiled))
        with open(self.compiled, 'r') as f:
            data = json.loads(f.read()) # Conversion du JSON en Python dict

        if verbose:
            print('-> Lecture terminée du fichier: {0}'.format(self.compiled))

        self.ABI = data.get('abi')
        self.BIN = data.get('bin')

        if verbose:
            print('-> Extraction du ABI et de BIN faite avec succès!')


    def deploy(self, verbose=False):
        '''Déploiement du Smart Contract'''
        self.read()
        if verbose:
            print('-> Déploiement du Smart Contract ...')
        instance = self.w3.eth.contract(abi=self.ABI, bytecode=self.BIN)
        if verbose:
            print('-> Préparation de la transaction du déploiement ...')
        # Hacky ... mais fonctionnel :D
        tx_data = instance.constructor().__dict__.get('data_in_transaction')
        transaction = {
            'from': self.pub, # Celui qui va envoyer la transaction
            'value': 0, # Combien d'ethers vont être envoyés durant la transaction
            'gas': 2000000, # GAS ... J'essaie de le rendre dynamique ...
            'gasPrice': self.w3.eth.gasPrice, # GAS price dynamique
            'nonce': self.w3.eth.getTransactionCount(pub), # Le nonce de l'adresse
            'data': tx_data # Les données envoyées durant la transaction
        }
        if verbose:
            print('-> Signer la transaction avec la clé privée...')

        signed = self.w3.eth.account.signTransaction(transaction, self.priv)

        if verbose:
            print('-> Envoyer la transaction signée aux noeuds du provider ...')

        tx_hash = self.w3.eth.sendRawTransaction(signed.rawTransaction)

        if verbose:
            print('-> Transaction envoyée avec succès avec ce hash: {0}'.format(tx_hash.hex()))

        # Convertir le ByteArray en hexadécimal
        return tx_hash.hex()

    def wait_for_receipt(self, tx_hash, contract=False, verbose=False):
        '''Attendre au moins une confirmation de la transaction'''
        if verbose:
            print('-> Attente de la première confirmation de la transaction: {0}'.format(tx_hash))
            print('...')
        while True:
            # Avoir les informations de la transaction
            # Y compris l'adresse du Smart Contract
            tx_receipt = self.w3.eth.getTransactionReceipt(tx_hash)
            if tx_receipt:
                if verbose:
                    print('-> Transaction confirmée!')
                if contract:
                    # Avoir l'adresse du Smart Contract
                    contract_address = tx_receipt.get('contractAddress')
                    if verbose:
                        print("-> Écriture de l'adresse du contract '{0} dans le fichier: {1}'".format(
                            contract_address, 'contract_address')
                        )

                    with open('contract_address', 'w') as f:
                        f.write(str(contract_address))

                    if verbose:
                        print('-> Écriture terminée avec succès!')

                    return contract_address
                else:
                    return None
            # Attendre 1 seconde puis tester si la transaction est confirmée ou non
            time.sleep(1)

    def interact(self, contract_address, abi, new_msg=''):
        '''Interaction avec le Smart Contract Greeter déployé'''
        # Création d'une instance du Smart Contract Greeter
        greeter = self.w3.eth.contract(address=contract_address, abi=abi)
        # Appel de la fonction greet() dans le Smart Contract déployé Greeter
        msg = greeter.functions.greet().call()
        print('Actual message: ', msg)
        if new_msg:
            # Appel de la fonction setGreeting(string _greeting) en ajoutant un argument
            # Cette fonction modifie l'état d'une variable dans le Smart contract
            # => Faut payer des frais de transaction de cette modification
            # ==> Faut créer une transaction et la signer avec la clé privée
            # de celui qui va faire cette modification dans le Smart contract
            tx_data = greeter.functions.setGreeting(new_msg).buildTransaction({
                'gasPrice': self.w3.eth.gasPrice, # GAS Price dynamique
                'nonce': self.w3.eth.getTransactionCount(self.pub), # Le nonce de l'adresse
            })
            # Signer la transaction avec l'adresse privée
            signed_tx = self.w3.eth.account.signTransaction(tx_data, self.priv)
            # Envoyer la transaction signée aux noeuds du provider
            tx_hash = self.w3.eth.sendRawTransaction(signed_tx.rawTransaction)
            # Attendre au moins une confirmation
            self.wait_for_receipt(tx_hash.hex())
            # Appel de la fonction greet() dans le Smart Contract
            msg = greeter.functions.greet().call()
            print('New message: ', msg)  

test_compile.py:

# Tester la compilation d'un Smart contract
# Author: Chiheb Nexus - 2018
# License: GPLv3
#

from compile_contract import CompileContract

path_file = 'contract.sol' # Le nom du fichier
contract_name = 'Greeter' # Le nom du contract
contract = CompileContract(path_file=path_file, contract_name=contract_name)
contract.compile(verbose=True)

Et en ouvrant la terminale:

(eth_contract) ┌─[Chiheb@NeXus]─[~/tutos/eth_contract/working_with_contracts]
└──╼ [>] python3 test_compile.py 
-> Compilation en cours ...
-> Lecture du fichier: contract.sol
-> Lecture terminée! du fichier: contract.sol
-> Écriture du contract compilé dans le fichier: compiled_contract
-> Écriture terminée avec succès! dans le fichier: compiled_contract
-> Compilation terminée avec succès!

Et le contenu du fichier compiled_contract:

(eth_contract) ┌─[Chiheb@NeXus]─[~/tutos/eth_contract/working_with_contracts]
└──╼ [>] cat compiled_contract

{"userdoc": "{\"methods\":{}}", "asm": {".code": [{"value": "80", "name": "PUSH", "begin": 26, "end": 314}, {"value": "40", "name": "PUSH", "begin": 26, "end": 314}, {"name": "MSTORE", "begin": 26, "end": 314}, 
...
...
SWAP1 JUMP STOP LOG1 PUSH6 0x627A7A723058 KECCAK256 0x5e 0xe8 PC CALLVALUE DUP5 PUSH25 0xE19FD87537C88E9C6E4AE2284E9E3F06B13E9DFFF92C21303D DUP9 STOP 0x29 "}

Donc, comme on peut le voir, la compilation a été faite avec succès et les résultats de la compilation ont été enregistrées dans un fichier nommée compiled_contract.

Maintenant, testons le déploiement du Smart contract sur le network ropsten: testnet

NB: Avant de tester ce code, veuillez créer une adresse Ethereum (Public & Private keys) et l’alimenter avec des ethers du réseau ropsten. Vous pouvez utiliser le “faucet” de metamask via ce lien: https://faucet.metamask.io/

test_deploy.py:

# Tester la compilation d'un Smart contract
# Author: Chiheb Nexus - 2018
# License: GPLv3
#

from interact_contract import InteractWithContract

# inscrivez-vous au site de infura.io 
# et avoir une clé
provider = 'https://ropsten.infura.io/YOUR_INFURA_KEY'
# adresse publique ethereum
pub = 'YOUR_PUBLIC_ADDRESS'
# la clé privée de l'adresse publique
key = 'YOUR_PRIVATE_ADDRESS'
# fichier contenant l'ouput de la compilation du Smart contract
compiled_contract_source = 'compiled_contract'
instance = InteractWithContract(
    compiled_sol_path=compiled_contract_source, 
    public_key=pub, 
    private_key=key, 
    provider=provider
)
# déploiement du Smart Contract
tx_hash = instance.deploy(verbose=True)
# Attendre au moins une confirmation pour avoir l'adresse du Smart contract
contract_address = instance.wait_for_receipt(tx_hash, contract=True, verbose=True)

Sur la terminale:

(eth_contract) ┌─[✗]─[Chiheb@NeXus]─[~/tutos/eth_contract/working_with_contracts]
└──╼ [>] python3 test_deploy.py 
-> Déploiement du Smart Contract ...
-> Préparation de la transaction du déploiement ...
-> Signer la transaction avec la clé privée...
-> Envoyer la transaction signée aux noeuds du provider ...
-> Transaction envoyée avec succès avec ce hash: 0xca282f659024d79d376c5d5511543480ee64435abe9d9d6ffec426d815f5338f
-> Attente de la première confirmation de la transaction: 0xca282f659024d79d376c5d5511543480ee64435abe9d9d6ffec426d815f5338f
...
-> Transaction confirmée!
-> Écriture de l'adresse du contract '0x47C252818FA567d8AE9F06bDAfbfA5987072E64B' dans le fichier: contract_address'
-> Écriture terminée avec succès!

Comme on peut voir, le déploiement a été fait avec succès et on a pu avoir l’adresse du Smart contract déployé qui est: 0x47C252818FA567d8AE9F06bDAfbfA5987072E64B

Maintenant, essayons de nous interagir avec le Smart Contract déployé:

test_interact.py:

# Tester la compilation d'un Smart contract
# Author: Chiheb Nexus - 2018
# License: GPLv3
#

from interact_contract import InteractWithContract

# inscrivez-vous au site de infura.io 
# et avoir une clé
provider = 'https://ropsten.infura.io/YOUR_INFURA_KEY'
# adresse publique ethereum
pub = 'YOUR_PUBLIC_ADDRESS'
# la clé privée de l'adresse publique
key = 'YOUR_PRIVATE_ADDRESS''
compiled_contract_source = 'compiled_contract'
instance = InteractWithContract(
    compiled_sol_path=compiled_contract_source, 
    public_key=pub, 
    private_key=key, 
    provider=provider
)

# ABI du contract compilé
ABI = [{'name': 'setGreeting', 'constant': False, 'type': 'function', 'stateMutability': 'nonpayable', 'inputs': [{'name': '_greeting', 'type': 'string'}], 'payable': False, 'outputs': []}, {'name': 'greet', 'constant': True, 'type': 'function', 'stateMutability': 'view', 'inputs': [], 'payable': False, 'outputs': [{'name': '', 'type': 'string'}]}, {'name': 'greeting', 'constant': True, 'type': 'function', 'stateMutability': 'view', 'inputs': [], 'payable': False, 'outputs': [{'name': '', 'type': 'string'}]}, {'inputs': [], 'payable': False, 'type': 'constructor', 'stateMutability': 'nonpayable'}]
# adresse du contract après déploiement
contract_address = '0x47C252818FA567d8AE9F06bDAfbfA5987072E64B'
instance.interact(contract_address=contract_address, abi=ABI, new_msg='Chiheb Nexus')

Sur la terminale:

(eth_contract) ┌─[Chiheb@NeXus]─[~/tutos/eth_contract/working_with_contracts]
└──╼ [>] python3 test_interact.py 
Actual message:  Hello
New message:  Chiheb Nexus

Et voilà ! Les tests sont faits avec succès ! :D

Bonus: Vérification des informations sur etherscan :D

Tout d’abord, allons sur l’adresse de notre Smart contract https://ropsten.etherscan.io/address/0x47c252818fa567d8ae9f06bdafbfa5987072e64b (Noter l’URL: ropsten.etherscan.io, car notre exemple fonctionne sur la testnet Ropsten).

PS:: Noter que l’adresse de vos Smart contracts vont être différentes de l’adresse du Smart Contract que j’ai déployé

1. Clic sur “Code” puis “Verify and Publish”

2. Remplir les cases comme suit

3. Puis clic sur Verify and Publish

4. Et voilà ! Notre Smart Contract est vérifié et validé sur etherscan

5. Allons sur l’adresse de notre Smart contract

Cheers :D !