Deux libraries essentielles : - web3.py : client Python pour Ethereum (deploiement, appels, events) - py-solc-x : wrapper Python autour du compilateur Solidity solc
# Installation (decommentez si necessaire)# !pip install web3 py-solc-ximport importlibfor pkg in ["web3", "solcx"]:try: mod = importlib.import_module(pkg) version =getattr(mod, "__version__", "?")print(f"{pkg:10s} : v{version}")exceptImportError:print(f"{pkg:10s} : NON INSTALLE - executez: pip install {'py-solc-x'if pkg =='solcx'else pkg}")
web3 : v7.16.0
solcx : v?
Installation du compilateur Solidity via py-solc-x pour permettre la compilation de contrats directement depuis Python.
# Installer le compilateur Solidity via py-solc-xtry:import solcxexceptImportErroras e:print(f"Installation requise : pip install py-solc-x")print(f"Erreur : {e}")# Installer la derniere version stable de solcSOLC_VERSION ="0.8.28"try: installed = solcx.get_installed_solc_versions()print(f"Versions solc installees : {installed}")ifnotany(str(v).startswith(SOLC_VERSION) for v in installed):print(f"Installation de solc {SOLC_VERSION}...") solcx.install_solc(SOLC_VERSION) solcx.set_solc_version(SOLC_VERSION)print(f"Compilateur actif : solc {solcx.get_solc_version()}")exceptExceptionas e:print(f"Erreur installation solc : {e}")print("Verifiez votre connexion internet et reessayez.")
py-solc-x telecharge et gere les binaires solc automatiquement. Cela nous permet de compiler du Solidity directement depuis Python, sans avoir besoin d’un IDE comme Remix ou d’un framework comme Hardhat.
2. Connexion a anvil (blockchain locale)
anvil est le noeud Ethereum local de Foundry. Il simule une blockchain complete en memoire avec 10 comptes pre-finances (10 000 ETH chacun).
Demarrer anvil
Dans un terminal separe, lancez :
anvil
Anvil ecoute par defaut sur http://127.0.0.1:8545.
try:from web3 import Web3exceptImportErroras e:print(f"Installation requise : pip install web3")print(f"Erreur : {e}")# Connexion a anvil (blockchain locale)ANVIL_URL ="http://127.0.0.1:8545"w3 = Web3(Web3.HTTPProvider(ANVIL_URL))if w3.is_connected():print(f"Connecte a : {ANVIL_URL}")print(f"Chain ID : {w3.eth.chain_id}")print(f"Bloc actuel: {w3.eth.block_number}")print(f"Gas price : {w3.eth.gas_price} wei")print()# Comptes disponibles (pre-finances par anvil) accounts = w3.eth.accountsprint(f"Comptes disponibles : {len(accounts)}")for i, account inenumerate(accounts[:3]): balance = w3.eth.get_balance(account)print(f" [{i}] {account} : {w3.from_wei(balance, 'ether'):.0f} ETH")print(f" ... ({len(accounts) -3} autres comptes)")else:print(f"ERREUR : Impossible de se connecter a {ANVIL_URL}")print("Avez-vous lance 'anvil' dans un terminal separe ?")
Connecte a : http://127.0.0.1:8545
Chain ID : 31337
Bloc actuel: 0
Gas price : 2000000000 wei
Comptes disponibles : 10
[0] 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 : 10000 ETH
[1] 0x70997970C51812dc3A010C7d01b50e0d17dc79C8 : 10000 ETH
[2] 0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC : 10000 ETH
... (7 autres comptes)
Interpretation : anvil, un environnement de test gratuit et instantane
Anvil fournit un environnement de test gratuit et instantane : - Pas besoin de vrais ETH (comptes pre-finances) - Blocs mines instantanement (pas d’attente) - Etat reinitialise a chaque redemarrage - Compatible avec tous les outils Ethereum (web3.py, ethers.js, Metamask)
C’est l’equivalent d’un Ganache modernise, integre a Foundry.
3. Compiler un contrat Solidity depuis Python
Avec py-solc-x, on compile du Solidity et on recupere l’ABI (interface) et le bytecode.
Interpretation : un contrat deploye, adresse, code et etat
Le contrat est maintenant vivant sur la blockchain locale. Il a : - Une adresse unique (hash de l’adresse du deployeur + nonce) - Son code stocke dans la blockchain (le bytecode) - Son etat initialise (la variable message = “Bonjour depuis Python !”)
Toute interaction future se fera via cette adresse.
5. Interagir avec le contrat
Deux types d’appels : - call() : lecture seule (gratuit, pas de transaction) - transact() : modification d’etat (consomme du gas, créé une transaction)
# Creer une instance du contrat a son adressehello = w3.eth.contract(address=contract_address, abi=abi)# LECTURE : appeler getMessage() (gratuit, pas de gas)message = hello.functions.getMessage().call()print(f"Message actuel : '{message}'")print()# ECRITURE : appeler setMessage() (transaction, consomme du gas)tx_hash = hello.functions.setMessage("Hello from web3.py !").transact({"from": deployer,})receipt = w3.eth.wait_for_transaction_receipt(tx_hash)print(f"setMessage() execute :")print(f" Gas utilise : {receipt.gasUsed:,}")print(f" Status : {'OK'if receipt.status ==1else'ECHEC'}")print()# Relire le messagenew_message = hello.functions.getMessage().call()print(f"Nouveau message : '{new_message}'")print()# On peut aussi lire la variable publique directementdirect = hello.functions.message().call()print(f"Lecture directe : '{direct}'")assert direct == new_message, "Les deux lectures doivent etre identiques"
Message actuel : 'Bonjour depuis Python !'
setMessage() execute :
Gas utilise : 28,234
Status : OK
Nouveau message : 'Hello from web3.py !'
Lecture directe : 'Hello from web3.py !'
Interpretation : call() et transact(), gas et etat
Opération
Méthode
Gas
Modifie l’etat
Lecture
.call()
0
Non
Ecriture
.transact()
> 0
Oui
Les variables public generent automatiquement un getter (ici message()). C’est pourquoi getMessage() et message() retournent la même valeur.
6. Pattern helper reutilisable
Pour les notebooks suivants, nous utiliserons un helper qui encapsule la connexion, la compilation et le deploiement en quelques lignes.
def compile_and_deploy(w3, source_code, deployer, *constructor_args):"""Compiler et deployer un contrat Solidity en une seule fonction. Args: w3: Instance Web3 connectee source_code: Code Solidity complet deployer: Adresse du compte deployeur *constructor_args: Arguments du constructeur Returns: tuple: (contract_instance, tx_receipt) """import solcx# Compiler compiled = solcx.compile_source( source_code, output_values=["abi", "bin"], solc_version=SOLC_VERSION, ) contract_id, contract_interface = compiled.popitem() abi = contract_interface["abi"] bytecode = contract_interface["bin"]# Deployer Contract = w3.eth.contract(abi=abi, bytecode=bytecode) tx_hash = Contract.constructor(*constructor_args).transact({"from": deployer}) receipt = w3.eth.wait_for_transaction_receipt(tx_hash)# Retourner l'instance du contrat instance = w3.eth.contract(address=receipt.contractAddress, abi=abi)return instance, receipt# Test du helper avec un contrat CounterCOUNTER_SOL ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Counter { uint256 public count; function increment() public { count += 1; } function decrement() public { require(count > 0, "Cannot go below zero"); count -= 1; }}"""counter, receipt = compile_and_deploy(w3, COUNTER_SOL, deployer)print(f"Counter deploye a : {counter.address}")print(f"Gas: {receipt.gasUsed:,}")print()# Utilisationprint(f"count = {counter.functions.count().call()}")for i inrange(5): counter.functions.increment().transact({"from": deployer})print(f"Apres 5 increments : count = {counter.functions.count().call()}")counter.functions.decrement().transact({"from": deployer})print(f"Apres 1 decrement : count = {counter.functions.count().call()}")
TRANSFERT ETH
============================================================
Alice (0xf39Fd6e5...) :
Avant : 9999.9984 ETH
Apres : 9998.9984 ETH
(envoye 1 ETH + gas)
Bob (0x70997970...) :
Avant : 10000.0000 ETH
Apres : 10001.0000 ETH
(recu 1 ETH)
Gas utilise : 21,000 (0.000027 ETH)
8. Exemple guide : Deployer et tester un contrat Vault
Resolu par le groupe Antoine Gaillard & Ambroise Durst (contribution PR #2397).
Compilez, deployez et testez un coffre-fort Solidity depuis Python.
# Exercice : Contrat Vault (coffre-fort)VAULT_SOL ="""// SPDX-License-Identifier: MITpragma solidity ^0.8.28;contract Vault { mapping(address => uint256) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw(uint256 amount) public { require(balances[msg.sender] >= amount, "Insufficient balance"); balances[msg.sender] -= amount; payable(msg.sender).transfer(amount); } function getBalance() public view returns (uint256) { return balances[msg.sender]; }}"""# Verifier la connexion a anvil avant de deployerifnot w3.is_connected():raiseConnectionError("Impossible de se connecter a anvil sur http://127.0.0.1:8545. ""Lancez `anvil` dans un terminal separe puis relancez cette cellule." )# Deployer le contrat Vaultvault, receipt = compile_and_deploy(w3, VAULT_SOL, deployer)print(f"Vault deploye a : {vault.address}")print(f"Gas de deploiement : {receipt.gasUsed:,}")print()# Comptes Alice et Bobalice = w3.eth.accounts[1]bob = w3.eth.accounts[2]# Deposer 2 ETH depuis Aliceprint("Depot 2 ETH depuis Alice")tx_hash = vault.functions.deposit().transact({"from": alice,"value": w3.to_wei(2, "ether"),})receipt = w3.eth.wait_for_transaction_receipt(tx_hash)print(f" Tx hash : {tx_hash.hex()}")print(f" Gas utilise : {receipt.gasUsed:,}")print()# Deposer 1 ETH depuis Bobprint("Depot 1 ETH depuis Bob")tx_hash = vault.functions.deposit().transact({"from": bob,"value": w3.to_wei(1, "ether"),})receipt = w3.eth.wait_for_transaction_receipt(tx_hash)print(f" Tx hash : {tx_hash.hex()}")print(f" Gas utilise : {receipt.gasUsed:,}")print()# Verifier les soldes dans le vaultbalance_alice = vault.functions.getBalance().call({"from": alice})balance_bob = vault.functions.getBalance().call({"from": bob})print("Soldes dans le vault :")print(f" Alice : {w3.from_wei(balance_alice, 'ether'):.3f} ETH")print(f" Bob : {w3.from_wei(balance_bob, 'ether'):.3f} ETH")print()# Alice retire 0.5 ETHprint("Alice retire 0.5 ETH")tx_hash = vault.functions.withdraw(w3.to_wei(0.5, "ether")).transact({"from": alice,})receipt = w3.eth.wait_for_transaction_receipt(tx_hash)print(f" Tx hash : {tx_hash.hex()}")print(f" Gas utilise : {receipt.gasUsed:,}")print()# Verifier le solde final d'Alicefinal_balance_alice = vault.functions.getBalance().call({"from": alice})print("Solde final d'Alice dans le vault :")print(f" Alice : {w3.from_wei(final_balance_alice, 'ether'):.3f} ETH")assert final_balance_alice == w3.to_wei(1.5, "ether"), "Le solde final d'Alice doit etre 1.5 ETH"print("Verification : le solde final d'Alice est bien 1.5 ETH")
Vault deploye a : 0x610178dA211FEF7D417bC0e6FeD39F05609AD788
Gas de deploiement : 325,118
Depot 2 ETH depuis Alice
Tx hash : 3ae6f559b30f6d24ce9d62e82bb8fb81c39181a15ceab4e2d6a55b6b346d3688
Gas utilise : 43,623
Depot 1 ETH depuis Bob
Tx hash : 52428b2b8f9d6805cd0dd3523838c2afaa8d95e9317539c94e5273a78b02fc23
Gas utilise : 43,623
Soldes dans le vault :
Alice : 2.000 ETH
Bob : 1.000 ETH
Alice retire 0.5 ETH
Tx hash : d835f309d9effe485207e76f5b7ab73ba9770f358ae2b9cfe18df30fa9f45324
Gas utilise : 34,098
Solde final d'Alice dans le vault :
Alice : 1.500 ETH
Verification : le solde final d'Alice est bien 1.5 ETH
Exercice (a completer) : Événements et plafond de depot
En vous inspirant de l’exemple guide ci-dessus, faites evoluer le contrat Vault :
Ajoutez deux événements Solidity Deposit(address indexed compte, uint256 montant) et Withdrawal(address indexed compte, uint256 montant), et emettez-les dans deposit() et withdraw().
Ajoutez un plafond : refusez (require) tout depot qui ferait depasser un solde individuel de 5 ETH.
Redeployez le contrat, effectuez un depot valide puis un depot qui depasse le plafond (attrapez l’exception), et lisez les événements via contract.events.Deposit().get_logs(...).
# Exercice : Evenements et plafond de depot sur le contrat Vault# TODO etudiant :# Etape 1 : copier VAULT_SOL et ajouter les evenements Deposit / Withdrawal + le plafond (require <= 5 ETH)# Etape 2 : recompiler et redeployer avec compile_and_deploy(w3, VAULT_PLAFOND_SOL, deployer)# Etape 3 : tester un depot valide, puis un depot au-dela du plafond (try/except)# Etape 4 : lire les evenements emis via vault.events.Deposit().get_logs(from_block=0)print("Exercice a completer")
Exercice a completer
Exercice (a completer) : Protection anti-reentrance sur le contrat Vault
Le contrat Vault de l’exemple guide (cellule precedente) presente une faille classique de reentrance : dans withdraw(), le transfert ETH vers l’appelant (payable(...).transfer(amount)) intervient avant la mise a jour du solde, ce qui permet a un contrat malveillant de rappeler withdraw() pendant le transfert et de vider les fonds.
Objectif : appliquer le pattern Checks-Effects-Interactions via un guard de reentrance.
Ajoutez une variable d’etat bool public locked; (initialement false).
Definissez un modifier nonReentrant qui reverte si locked est true, pose le verrou avant l’effet, et le relache apres : require(!locked, "Reentrant"); locked = true; _; locked = false;.
Appliquez nonReentrant a deposit() et withdraw().
Redeployez avec compile_and_deploy(w3, VAULT_REENTRANCY_SOL, deployer) puis verifiez qu’une deuxieme appel imbrique est bien bloque (scenario de test).
Indice : l’ordre Checks -> Effects (mise a jour de balances) -> Interactions (transfer) est tout aussi important que le guard lui-meme ; le modifier protege, l’ordre elimine la race.
# Exercice : Protection anti-reentrance sur le contrat Vault# TODO etudiant :# Etape 1 : copier VAULT_PLAFOND_SOL (ou VAULT_SOL) et ajouter un guard de reentrance# - variable d'etat bool locked (init a false)# - modifier nonReentrant { require(!locked, "Reentrant"); locked = true; _; locked = false; }# - appliquer nonReentrant a deposit() et withdraw()# Etape 2 : recompiler et redeployer avec compile_and_deploy(w3, VAULT_REENTRANCY_SOL, deployer)# Etape 3 : verifier que le guard bloque une tentative de reentrance (scenario de test)print("Exercice a completer")
Exercice a completer
Exercice (a completer) : call() vs transact() – lecture vs ecriture
Web3.py distingue deux modes d’interaction avec un contrat deploye, et la confusion entre les deux est une source frequentes de bugs. Cet exercice vise a bien ancrer la difference.
Ecrivez un contrat LectureEcriture qui stocke un uint256 public valeur; et expose :
une fonction viewgetValeur() returns (uint256) (lecture seule, hors frais),
une fonction writesetValeur(uint256 v) (modifie l’etat, coute du gas).
Deployez avec compile_and_deploy(w3, LECTURE_ECRITURE_SOL, deployer).
Lisez la valeur initiale via .call() (transaction simulee, pas de gas, pas de modification d’etat – c’est l’equivalent d’un eth_call).
Modifiez la valeur via .transact() (transaction reelle minee, coute du gas, modifie l’etat – equivalent d’un eth_sendTransaction).
Relisez via .call() pour confirmer que l’etat a change.
Indice : une fonction marquee view ou pure en Solidity peut etre appelee gratuitement via .call() ; toute fonction qui ecrit ne peut l’etre que via .transact(). Confondre les deux sur une fonction d’ecriture = la transaction echoue silencieusement (pas d’effet) ou est rejectee.
# Exercice : call() vs transact() -- lecture (view) vs ecriture (state change)# TODO etudiant :# Etape 1 : ecrire un contrat LectureEcriture avec une fonction view getValeur() et une fonction setValeur(uint256 v)# Etape 2 : deployer via compile_and_deploy(w3, LECTURE_ECRITURE_SOL, deployer)# Etape 3 : lire la valeur initiale avec .call() (pas de gas, pas de modification d'etat)# Etape 4 : modifier la valeur avec .transact() (coute du gas, modifie l'etat)# Etape 5 : relire via .call() pour confirmer le changement d'etatprint("Exercice a completer")
Ce notebook a etabli le pont entre Python et la blockchain en configurant les deux outils essentiels du workflow de développement : web3.py pour interagir avec Ethereum et py-solc-x pour compiler du Solidity directement depuis Python. Le parcours complet – compilation d’un contrat source Solidity en ABI et bytecode, deploiement via transaction sur le noeud local anvil, puis interactions en lecture (.call()) et en ecriture (.transact()) – constitue le pattern reutilisable qui servira de base a tous les notebooks suivants de la serie.
Le helper compile_and_deploy() encapsule toute la complexite de compilation et deploiement en un seul appel, permettant de se concentrer sur la logique Solidity plutot que sur la plomberie. La demonstration des transactions multi-comptes avec transfert d’ETH entre Alice et Bob a illustre le modèle de gas et la différence entre opérations de lecture (gratuites) et d’ecriture (consommant du gas). L’exercice du contrat Vault introduit les mappings et le pattern payable qui seront approfondis dans les notebooks suivants.
Avec cet environnement operationnel, le notebook suivant SC-03-Solidity-Basics-Python entre dans le coeur du langage Solidity en explorant les types de données (bool, uint, int, address, bytes, string), les variables d’etat et locales, et les conversions de types, le tout compile et deploye reellement sur anvil.