Écrire une application décentralisée pour Ethereum – Partie 1 : le smart contract

lego-keyboard

Les possibilités offertes par la blockchain en terme de sécurité, de transparence et de confiance sont à présent reconnues de tous et notamment des médias les plus généralistes. Mais là où Ethereum apporte une nouvelle dimension, c’est dans ce qu’il offre pour intégrer dans la blockchain non pas uniquement des données (transactions monétaires, horodatage de documents…) mais également des programmes, dont l’exécution peut être contrôlée et certifiée au même titre qu’une transaction de Bitcoin.

En tant que développeur, lorsque j’ai découvert Ethereum et compris les possibilités qu’offraient les smart contracts (lire l’article « Smart contract », où le contrat auto-exécutant), ma nouvelle obsession a été de comprendre comment cela fonctionnait techniquement, et comment créer ma propre application décentralisée.

Cette série d’article sera consacrée à l’écriture d’une application entièrement décentralisée profitant des possibilités d’Ethereum. Elle s’adresse plus spécifiquement aux développeurs (web notamment, mais pas nécessairement) qui souhaitent découvrir les possibilités offertes par Ethereum. J’ai choisi de faire reposer ces articles sur un exemple concret d’application, en l’occurrence un jeu de roulette (extrêmement simplifié) de ceux que l’on trouve dans les casinos.

Mais avant de commencer à écrire notre application, voyons d’abord comment s’organise une application décentralisée sur Ethereum, et comment va s’organiser la suite.

Constitution d’une dApp

Une application décentralisée (aussi appelée dApp) est généralement accessible depuis un site web. Rien de très nouveau ici me direz-vous, c’est ce qui se fait dans le web depuis son commencement. Ce qui est nouveau en revanche, c’est que cette application web se connecte non pas à un serveur centralisée (serveur web) mais directement à la blockchain d’Ethereum. Cette connexion se fait en se connectant via une API à une application tournant sur le poste de l’utilisateur : son portefeuille.

Autrement dit, sur une application web classique (moderne) :

  • l’utilisateur se connecte à un serveur web qui lui renvoie l’interface à afficher dans le navigateur (partie front-end) ;
  • l’interface se connecte à une API (partie back-end) en lui envoyant des données, qui lui renvoient à son tour d’autres données.

L’application fonctionne en mode centralisé, dans le sens où a priori tous les utilisateurs se connectent au même serveur web, et font donc confiance à ce serveur. Impossible a priori de vérifier que le serveur web centralisé traite les données comme cela est promis. De plus si le serveur tombe (piratage, fermeture de la société…), toutes les données sont potentiellement perdues.

Dans le cas d’une application décentralisée en revanche :

  • l’utilisateur se connecte à un serveur web qui lui renvoie l’interface à afficher dans le navigateur (partie front-end, jusque là pas de changement) ;
  • l’interface se connecte au portefeuille local de l’utilisateur via une API (web3, nous verrons cela plus tard) en créant un contrat ou en appelant une méthode d’un contrat spécifique ;
  • le portefeuille exécute le contrat dans la blockchain, puis renvoie le résultat au front-end.

Si l’on fait l’analogie avec une application web classique, on peut donc dire que le rôle de back-end est assuré par le portefeuille local, qui se connecte à la blockchain. Plus de notion de serveur centralisé donc, si ce n’est celui qui sert l’interface web. Mais cette interface web peut après tout être intégrée au sein d’une application desktop par exemple.

Les étapes de développement de notre dApp

Au fur et à mesure de cette série d’articles nous allons donc mettre en place une application décentralisée de bout en bout :

  • écrire le contrat : ce sera la base applicative de notre jeu de roulette, qui sera distribuée dans la blockchain ;
  • déployer le contrat dans une « blockchain locale », et le tester via la console du navigateur ;
  • écrire une interface web permettant de créer un jeu de roulette d’une part, et d’y jouer d’autre part ;
  • déployer le contrat dans la blockchain réelle, et rendre disponible l’application web pour que d’autres puissent jouer.

Mise en garde : un contrat Ethereum peut contenir d’importantes failles de sécurité, d’autant plus que la technologie est très jeune. Cela est d’autant plus risqué que l’on manipule indirectement de l’argent (de l’Ether, échangeable contre des euros). Le jeu développé dans cette série d’articles l’est à but purement pédagogique ; en aucun cas je ne recommande de déployer dans la blockchain d’Ethereum contrat (et encore moins un jeu d’argent) sans avoir pris toutes les précautions nécessaires en matière d’audit du code par un expert en sécurité.

Outils de développement nécessaires

Afin de développer une dApp, nul besoin d’installer une artillerie lourde d’outils, l’environnement reste assez léger. Deux outils sont à installer dans un premier temps :

  • Truffle : il s’agit d’une petite suite de développement créée par ConsenSys comprenant le nécessaire pour compiler très facilement un contrat, le déployer dans la blockchain, et générer un squelette de dApp permettant d’y accéder ;
  • Test RPC : un petit client permettant de simuler un portefeuille local connecté à une blockchain locale également ; idéal pour les tests 🙂

À noter que l’installation de ces outils nécessite l’installation préalable de Node.js. Une fois ces outils installés, il est temps de passer à la première étape du développement de notre application : le contrat.

Écrire le smart contract pour Ethereum

Plongeons dans le vif du sujet avec l’écriture du contrat ; mais pas trop vite, prenons quelques minutes pour spécifier le comportement attendu de notre contrat dans le contexte de notre application de roulette.

Spécifications fonctionnelles du contrat

Techniquement, les contrats Ethereum sont stockés dans la blockchain sous forme de code binaire assemblé semblable au code machine exécuté par nos ordinateurs. Mais bien évidemment il existe un langage de plus haut niveau permettant l’écriture des contrats : Solidity. À mi-chemin entre le C et le JavaScript, il ne vous dépaysera pas si vous êtes un habitué des langages de programmation répandus.

roulette

Licence CC-BY-2.0 (Source)

Commençons par définir le comportement attendu de notre contrat. Le but est donc de gérer un jeu de roulette simplifié. Le principe est le suivant : à intervalles réguliers, un nombre aléatoire est choisi entre 0 et 36. Avant cela les joueurs ont le choix de miser :

  • soit sur un numéro (entre 0 et 36 donc) : le joueur gagne 35 fois sa mise si ce numéro tombe ;
  • soit sur la case « pair » : le joueur gagne deux fois sa mise si un numéro pair et différent de 0 tombe ;
  • soit sur la base « impair » : le joueur gagne deux fois sa mise si un numéro impair tombe.

Le jeu de roulette inclut également la possibilité de miser « à cheval » sur deux ou quatre numéros, sur des lignes, des colonnes, etc. mais nous nous en tiendrons pour l’instant à ces trois possibilités.

Notre contrat devra donc :

  • être créé avec comme paramètre l’intervalle de temps entre deux tours ;
  • recevoir les paris des joueurs : chacun est constitué d’un type (numéro simple single, pair even ou impair odd), du numéro le cas échéant, et du montant misé ;
  • lorsque le temps est écoulé, choisir un numéro aléatoire entre 0 et 36, et payer les joueurs gagnants.

Rien de très compliqué, mais certains points méritent quelques précisions. Tout d’abord les mises des joueurs et le paiement des gagnants sera fait directement grâce à de l’Ether inclut dans les transactions. Il aurait été possible d’utiliser une cryptomonnaie personnalisée comme cela est assez facile à implémenter avec Ethereum, mais il est intéressant de présenter l’intégration de l’Ether dans un contrat.

Deuxième point : il n’est pas possible nativement de programmer l’appel à une méthode d’un contrat. Il faut nécessairement qu’un utilisateur ou un autre contrat l’appelle. C’est pourquoi notre contrat donnera la possibilité à tout le monde de « tourner » la roulette et ainsi déclencher les paiements aux gagnants, tant que l’intervalle défini entre deux tours est bien écoulé. Ainsi chacun sait que ses mises ne seront pas bloquées parce que la roulette n’a pas été lancée.

Dernier point : il n’est pas non plus possible de tirer un nombre aléatoire dans un contrat Ethereum. En effet, le principe même de la blockchain veut que le résultat final de l’exécution soit le même quelque soit le nœud ayant procédé à l’exécution. Pour simuler ce tirage, nous utiliserons donc le hash du dernier block reçu avant l’exécution. Théoriquement il est donc possible de « manipuler » le tirage, mais cela reste difficile et coûteux ; et pour sécuriser davantage, on peut toujours utiliser les 2, 3…n derniers blocs.

À noter que certaines vérifications seront effectuées à chaque pari demandé :

  • il faudra que la transaction comprenne de l’Ether (le montant de la mise) ;
  • il faudra que la banque dispose d’assez de réserve pour payer les joueurs en partant du principe que tous seront gagnants ; autrement dit il faudra que l’adresse du contrat dispose d’assez d’Ether pour payer tous les paris cumulés depuis le dernier tour si ceux-ci s’avèrent gagnants.

Notre contrat est spécifié, il est temps à présent de l’écrire.

Écrire le contrat

Tout d’abord, un contrat réside dans un bloc contract. Typiquement, chaque contrat réside dans un fichier .sol. Ici notre application ne contiendra qu’un seul contrat ; plaçons-le dans le fichier Roulette.sol.

contract Roulette {
	// ...
}

Variables et types de données

Commençons à présent par déclarer quelques variables :

uint public lastRoundTimestamp;
uint public nextRoundTimestamp;
uint _interval;
address _creator;
L'éditeur Atom dispose d'un paquet pour la coloration syntaxique de Solidity

L’éditeur Atom dispose d’un paquet pour la coloration syntaxique de Solidity

La syntaxe de déclaration d’une variable est la suivante : <type> [modifier] <name>. Ici nous avons donc trois variables de type uint (entier non signé, c’est-à-dire positif), dont deux sont publiques, ce qui signifie qu’il sera possible à tout le monde de demander au contrat leur valeur actuelle. Il s’agit ici de timestamps, en l’occurrence celui du dernier tour et du prochain tour.

La troisième est privée, donc inaccessible à l’extérieur du contrat. Il s’agit de l’intervalle en secondes entre deux tours. La dernière variable enfin est type address : elle contient l’adresse du créateur du contrat, c’est-à-dire le compte Ethereum qui a créé et lancé le jeu. Cette information nous servira car elle nous permettra lorsque l’on souhaitera arrêter la partie de verser les Ethers restants sur le compte du contrat au compte ayant lancé le jeu.

Il nous reste une dernière variable à déclarer : celle qui contiendra les paris des joueurs pour le tour en cours. Cette variable est de type tableau et contient un type d’objet plus complexe : des structures, elles-mêmes comportant des énumérations.

enum BetType { Single, Odd, Even }
struct Bet {
	BetType betType;
	address player;
	uint number;
	uint value;
}
Bet[] public bets;

Tout d’abord nous déclarons une énumération, comme cela existe dans plusieurs langages de programmation. Celle-ci contient les différents types de paris possibles : numéro simple, impair et pair.

La structure Bet déclarée ensuite représente un pari. Elle comprend donc :

  • le type du pari (en utilisant notre énumération BetType) ;
  • l’adresse du joueur ayant fait le pari ;
  • le numéro sur lequel a misé le joueur (non utilisé pour les paris de types pair et impair) ;
  • la valeur du pari, autrement dit la mise (en wei, fraction la plus petite d’Ether, un Ether comportant 10^18 weis, autrement dit un milliard de millards de weis).

Les paris sont stockés dans la variable bets, de type tableau de Bets. Notez qu’il a été décidé ici de rendre les paris publics (comme cela est le cas dans un vrai jeu de roulette : tout le monde peut voir la table de jeu avec les paris en cours).

Dernière chose à déclarer avant de passer aux fonctions du contrat : les évènements. Dans notre cas nous n’en aurons qu’un seul, déclenché lorsque le tour est terminé et qu’un numéro a été tiré :

event Finished(uint number, uint nextRoundTimestamp);

Nous déclarons ici que notre contrat est susceptible de déclencher cet évènement, en y associant deux paramètres de type entier. Il s’agira du numéro tiré, et du timestamp correspondant à l’heure du prochain tour.

C’est terminé pour les déclarations, passons maintenant au cœur du contrat : les fonctions.

Les fonctions de notre contrat

Dans Solidity un contrat peut en réalité être considéré comme une classe, comme on en trouve en programmation orientée objet. C’est-à-dire qu’à partir d’une classe on peut créer plusieurs objets (les instances de la classe), et que chacun aura son propre espace mémoire pour stocker les valeurs des variables que nous avons déclarées dans le paragraphe précédent.

Ainsi de la même manière que dans les autres langages orientés objets, on peut définir pour un contrat des méthodes, qui sont plutôt nommées fonctions dans Solidity.

Et l’une d’elles est particulière puisque c’est elle qui est automatiquement appelée à la création (ou plutôt l’instanciation) d’un contrat : le constructeur. Comme dans beaucoup de langages, elle a pour signe distinctif d’avoir un nom identique au contrat :

function Roulette(uint interval) {
	_interval = interval;
	_creator = msg.sender;
	nextRoundTimestamp = now + _interval;
}

Notre constructeur prend un paramètre interval de type entier non signé, qui nous sert à initialiser la variable _interval évoquée plus haut, contenant le nombre de secondes entre deux tours de jeu.

Le constructeur initialise également la variable _creator contenant l’adresse du créateur de la partie. Pour cela il accède à l’objet global msg contenant les informations de la transactions courantes, et en particulier sa propriété sender qui comprend l’adresse de l’émetteur de la transaction. Enfin la variable publique nextRoundTimestamp est initialisée à partir du timestamp courant.

Passons aux fonctions publiques permettant aux joueurs de faire des paris :

function betSingle(uint number) public transactionMustContainEther() bankMustBeAbleToPayForBetType(BetType.Single) {
	if (number > 36) throw;
	bets.push(Bet({
		betType: BetType.Single,
		player: msg.sender,
		number: number,
		value: msg.value
	}));
}

La fonction betSingle permet de miser sur un numéro. Elle est publique, c’est-à-dire qu’elle peut être appelée par n’importe qui. Nous reviendrons plus loin sur le reste de la première ligne ; il s’agit de modifiers qui nous permettent ici de vérifier que l’appel est autorisé, à savoir que la transaction contient de l’Ether (la valeur de la mise) et que la banque sera en mesure de payer si tous les joueurs gagnent.

La première ligne du corps de la fonction vérifie la validité du paramètre number : il faut qu’il soit inférieur ou égal à 36 (le fait qu’il soit supérieur ou égal à 0 est assuré par le type même du paramètre). Dans le cas contraire, par l’instruction throw nous arrêtons purement et simplement l’exécution de l’appel à la fonction. Si l’état du contrat avait été modifié (changement de valeur d’une variable), ces modifications sont annulées.

Puis on ajoute à la liste des paris en cours un nouveau pari. Notez à nouveau l’utilisation de msg.sender pour stocker l’adresse du joueur, à savoir l’adresse du compte à l’origine de la transaction, ainsi que de msg.value qui contient le solde en weis associé à la transaction, et qui correspond donc ici à la mise du pari.

On remarque donc que l’appel à une fonction d’un contrat n’est rien d’autre qu’une simple transaction. Celle-ci peut contenir une valeur en weis et/ou demander l’exécution d’une fonction sur un contrat. Elle peut également retourner une valeur, comme c’est le cas dans cette fonction permettant de connaître le nombre de paris en cours, et la valeur totale des paris :

function getBetsCountAndValue() public constant returns(uint, uint) {
	uint value = 0;
	for (uint i = 0; i < bets.length; i++) {
		value += bets[i].value;
	}
	return (bets.length, value);
}

En plus de la rendre publique, on déclare également que cette fonction ne modifie pas l’état du contrat (constant), et qu’elle renvoie une paire d’entiers non signés (returns(uint, uint)). Peu de choses à dire sur cette fonction, vous ne devriez pas avoir trop de mal à comprendre comment elle fonctionne !

Analysons à présent la fonction qui permet de faire tourner la roulette :

function launch() public {
	if (now < nextRoundTimestamp) throw;

	uint number = uint(block.blockhash(block.number - 1)) % 37;
	
	for (uint i = 0; i < bets.length; i++) {
		bool won = false;
		uint payout = 0;
		if (bets[i].betType == BetType.Single) {
			if (bets[i].number == number) {
				won = true;
			}
		} else if (bets[i].betType == BetType.Even) {
			if (number > 0 && number % 2 == 0) {
				won = true;
			}
		} else if (bets[i].betType == BetType.Odd) {
			if (number > 0 && number % 2 == 1) {
				won = true;
			}
		}
		if (won) {
			bets[i].player.send(bets[i].value * getPayoutForType(bets[i].betType));
		}
	}

	uint thisRoundTimestamp = nextRoundTimestamp;
	nextRoundTimestamp = thisRoundTimestamp + _interval;
	lastRoundTimestamp = thisRoundTimestamp;

	bets.length = 0;

	Finished(number, nextRoundTimestamp);
}

Rien de très compliqué à nouveau. On vérifie tout d’abord que l’opération est autorisée, c’est à dire si l’heure actuelle est bien supérieure à l’heure prévue pour le tour suivant. Dans le cas contraire, on arrête l’exécution.

On tire ensuite un nombre « aléatoire » : comme expliqué dans le paragraphe consacré aux spécifications du contrat, il n’est pas possible d’utiliser un nombre réellement aléatoire, puisque l’exécution doit être déterministe. Autrement dit, toute exécution du contrat dans la blockchain doit donner le même résultat. C’est pourquoi on se base sur le hash du dernier bloc généré, converti en entier, et auquel on applique un « modulo 37 », ce qui nous donne un entier compris entre 0 et 36 : uint(block.blockhash(block.number - 1)).

La suite consiste à parcourir chaque pari, et vérifier s’il est gagnant ou non en fonction de son type. Le cas échéant, on émet une transaction grâce à la méthode send applicable aux variables de type address . La fonction getPayoutForType nous permet ici de récupérer le coefficient multiplicateur de la mise si le pari est gagnant (×35 pour un numéro simple, ×2 pour les pairs/impairs) :

function getPayoutForType(BetType betType) constant returns(uint) {
	if (betType == BetType.Single) return 35;
	if (betType == BetType.Even || betType == BetType.Odd) return 2;
	return 0;
}

À la fin de notre fonction launch, on réinitialise les variables pour le prochain tour :

  • on définit la date du prochain tour ;
  • on vide le tableau des paris en cours (en lui définissant sa taille à zéro) ;

Enfin, on émet un évènement Finished (comme déclaré plus haut), contenant le numéro tiré, et la date du prochain tour.

Ajouter des comportements aux fonctions grâce aux modifiers

Revenons à la notion de modifier que nous avions mise de côté. Partons de la fonction betEven permettant de faire un pari de type « pair » :

function betEven() public transactionMustContainEther() bankMustBeAbleToPayForBetType(BetType.Even) {
	bets.push(Bet({
		betType: BetType.Even,
		player: msg.sender,
		number: 0,
		value: msg.value
	}));
}

Dans la déclaration de la fonction, transactionMustContainEther et bankMustBeAbleToPayForBetType sont des modifiers, c’est-à-dire des comportement assignés par défaut à la fonction. Examinons par exemple la déclaration du premier modifier :

modifier transactionMustContainEther() {
	if (msg.value == 0) throw;
	_
}

Finalement c’est très simple : un modifier ressemble en tout point à une fonction, sauf :

  • qu’il est déclaré avec le mot-clé modifier ;
  • que son corps comporte l’opérateur underscore _ : c’est à cet endroit que sera exécuté le code de la fonction modifiée.

Autrement dit, ici notre modifier ajoute une instruction au début des fonctions sur lesquels il est utilisé ; cette instruction déclenche une erreur si la transaction ne comporte pas d’Ether.

Le deuxième modifier est légèrement plus complexe, mais le principe est exactement le même :

modifier bankMustBeAbleToPayForBetType(BetType betType) {
	uint necessaryBalance = 0;
	for (uint i = 0; i < bets.length; i++) {
		necessaryBalance += getPayoutForType(bets[i].betType) * bets[i].value;
	}
	necessaryBalance += getPayoutForType(betType) * msg.value;
	if (necessaryBalance > this.balance) throw;
	_
}

De la même manière, il s’agit d’ajouter une vérification avant l’exécution de la fonction modifiée. On parcourt les paris en cours pour calculer la somme maximale que la banque devra payer si tous les joueurs gagnent. On y ajoute le pari demandé dans la transaction en cours, et si le nouveau montant est supérieur au solde de la banque, alors on refuse la transaction.

C’en est fini pour l’écriture du contrat ; vous pouvez retrouver ici son code complet. Il est temps désormais de le compiler et de le déployer dans la blockchain (de test pour le moment) afin de commencer à jouer un peu avec !

Compilation et test du contrat

Comme je l’ai précisé en introduction, nous allons tester notre contrat non pas dans la blockchain d’Ethereum, mais dans une sorte de blockchain locale de test, Test RPC, que vous avez dû installer. Il s’agit d’un petit logiciel, qui une fois lancé permet de recevoir des connexions provenant d’une dApp, comme s’il s’agissait de la blockchain d’Ethereum.

Découverte de Test RPC et de l’API web3

Pour lancer Test RPC, il suffit de taper la commande testrpc dans votre terminal :

$ testrpc
EthereumJS TestRPC v2.0.0

Available Accounts
==================
(0) 0xf8974c13f35c2309f9850f633d02f357e4d57601
(1) 0x7f867900fd1e2a9b5efd17c2e1deb12b4de4155a
(...)

Par défaut, Test RPC initialise sa blockchain avec 10 comptes (adresses), chacun étant crédité d’une quantité quasiment infinie d’Ether. Cela peut être très pratique, mais pour tester notre contrat, il sera préférable d’avoir une quantité plus raisonnable d’Ethers. Pour cela, il faut passer en paramètres des clés privées et les montants associés (en weis) au programme testrpc. Pour générer une clé privée, vous pouvez par exemple utiliser le site MyEtherWallet.

J’ai par exemple généré deux clés privées ; voici la commande permettant de lancer Test RPC en créditant ces deux adresses de 10 000 Ethers :

$ testrpc --account="0x40e16bbfe219ecaa733169b9192604338d0c55d48a3c90dae45badaede6822ff,10000000000000000000000" --account="0xf5d09e61086d537581377e29c93edcf19de73b42230c7b9fe390e4dd560ffc0a,10000000000000000000000"

testrpc screenshot

À présent que notre wallet local tourne, il s’agit de s’y connecter. Bien évidemment, au final la connexion sera réalisée par la partie web de notre application, mais pour le moment, tentons simplement d’accéder à une console qui nous permettra de tester notre contrat.

La méthode la plus simple selon moi est d’utiliser directement l’API JavaScript d’Ethereum : web3. C’est celle qui sera utilisée dans notre application, mais nous pouvons également l’utiliser indépendamment de toute interface web en passant par Node.js. Pour cela :

  1. Créez un nouveau répertoire quelque part sur votre machine, et rendez-y-vous dans votre terminal : mkdir -p ~/Documents/testweb3 ; cd $_ ;
  2. Installez localement le module web3 pour Node.js : npm install web3 ;
  3. Lancez l’invite de commande Node.js (node) et importez le module web3 : var Web3 = require('web3').

À présent, vous pouvez vous connecter à votre serveur Test RPC, et vérifiez que les adresses initialisées sont bien présentes, et disposent bien du bon solde d’Ether :

> var Web3 = require('web3');
> var web3 = new Web3(new Web3.providers.HttpProvider("http://localhost:8545"));
> web3.eth.accounts
[ '0x00ae1858ea41f5667cda17c7915c2f289c4ee819',
  '0x7f8105da4dd1af61da7bf587766103ba8534fcdc' ]
> web3.fromWei(web3.eth.getBalance(web3.eth.accounts[0]), "ether").toNumber()
10000

Grâce à web3.eth.accounts, nous accédons l’ensemble des comptes locaux pour lesquels nous disposons de la clé privée. Il s’agit d’un tableau contenant les adresses des comptes sous forme de chaîne de caractère.

La méthode web3.eth.getBalance permet tout simple de récupérer le solde en Ethers d’une adresse, en weis. Notez que dans la mesure où ces montants sont la plupart du temps très élevés (1 Ether = 10^18 weis), l’API web3 a recours à la bibliothèque bignumber.js, permettant comme son nom l’indique de manipuler des grands nombres. C’est pourquoi si vous appelez web3.eth.getBalance("<addr>"), vous n’aurez sans doute pas la valeur que vous espériez. Pour obtenir une valeur numérique, il faut utiliser la méthode toNumber() du résultat.

Enfin web3 propose aussi de convertir des valeurs en Ether, weis, etc. Dans l’exemple présenté plus haut, nous convertissons donc une valeur initialement en weis (retournée par getBalance) en Ethers.

Compilation et déploiement du contrat

Il est temps de compiler notre contrat avant de le déployer dans la blockchain. Pour cela nous allons utiliser l’outil Truffle. Théoriquement nous pouvons compiler et déployer notre contrat directement depuis la console de Node avec l’API web3, mais Truffle va grandement nous simplifier la tâche.

Truffle permet de créer facilement des dApps, en allant de la compilation du contrat jusqu’à la phase de build de l’application web (à l’instar de ce que font Grunt, Gulp et autre Webpack).

Tout d’abord il nous faut créer un projet Truffle. Pour cela, créez un répertoire vide, puis tapez-y la commande truffle init. Vous pouvez alors voir que Truffle a créé plusieurs répertoires et fichiers. Pour le moment, intéressons-nous à notre contrat. Supprimons donc purement et simplement le contenu du répertoire contracts, et créons-y un fichier Roulette.sol, contenant le code de notre contrat .

De plus, ouvrez le fichier truffle.json à la racine du répertoire, et remplacez la valeur de l’attribut deploy (actuellement « Metacoin ») par « Roulette ».

La compilation est ensuite très simple, il suffit de taper la commande truffle compile. Si tout se passe bien (ce qui devrait être le cas), déployer le contrat dans notre blockchain locale ne sera pas plus compliqué : truffle deploy. (Assurez-vous bien que testrpc est lancé, cela est nécessaire même pour la compilation du contrat.)

truffle screenshot

Test du contrat déployé

Pour tester notre contrat, il est possible de lancer une console Node.js dont l’environnement a été initialisé avec les contrats du projet. C’est-à-dire que les bibliothèques nécessaires à l’appel de l’API web3 ont déjà été importées. Encore mieux, lors de la compilation d’un contrat, Truffle génère un fichier (Roulette.sol.js) contenant en quelques sortes une classe Roulette dont les méthodes sont justement les méthodes publiques de notre contrat (ce n’est pas exactement cela, mais dans l’utilisation cela revient ça). De plus ce fichier contient également l’adresse à laquelle a été déployée le contrat lors de la commande truffle deploy. Pratique 😉

Pour lancer cette console, utilisez simplement la commande : truffle console

truffle console screenshot

Commençons donc par créer une partie de roulette. Pour cela nous appelons la méthode new de l’objet Roulette, qui appellera le constructeur de notre contrat. Les premiers paramètres correspondent aux paramètres du constructeur (dans notre cas il n’y en a qu’un seul, le nombre de secondes minimal entre deux parties), puis le paramètre suivant est un objet contenant les informations sur la transaction. (Rappelez-vous que dans Ethereum, tout appel à une méthode d’un contrat, ou même la création d’un contrat, est fait dans une transaction, ayant donc un émetteur, un destinataire, et éventuellement un solde d’Ether qui y est attaché.)

Dans notre cas l’émetteur sera la première des deux adresses que nous avons paramétrées dans testrpc. Le solde d’Ether attaché sera la réserve de la banque. Notez que rien ne nous empêche d’envoyer ce solde au contrat (qui dispose désormais d’une adresse qui lui est propre) dans une autre transaction.

La création de notre contrat se fait de manière asynchrone grâce aux promises de JavaScript, déclarons-donc d’abord une variable qui contiendra le contrat créé :

> var contract = null;

Puis appelons enfin la méthode Roulette.new afin de créer le contrat :

> Roulette.new(10, { from: web3.eth.accounts[0], value: web3.toWei(100, "ether") }).then(function(_contract) { contract = _contract; });

La création ne devrait pas prendre plus de quelques fractions de secondes, nous pouvons donc inspecter le contenu de l’objet contract (en tapant simple contract dans la console). Comme vous pouvez le voir, cet objet dispose notamment des méthodes betSingle, betEven, etc. correspondant aux méthodes de notre contrat Solidity. Notez également que les variables publiques sont accessibles par des méthodes également.

Consultons à présent le solde de notre contrat :

> web3.fromWei(web3.eth.getBalance(contract.address), "ether").toNumber()

En toute logique cela devrait vous retourner 100, puisque c’est le solde que nous avons envoyé au contrat.

Effectuons maintenant un pari avec l’adresse du deuxième compte paramétré dans testrpc :

> contract.betEven({ from: web3.eth.accounts[1], value: web3.toWei(1, "ether") })

Si vous reconsultez le solde du contrat, celui-ci dont être désormais de 101. Il ne nous reste qu’à faire tourner la roulette :

> contract.launch({ from: web3.eth.accounts[0] })

Pour le moment nous ne nous sommes pas abonnés aux évènements déclenchés par le contrat, il faut donc consulter les soldes de la banque (du contrat) ou du joueur pour savoir qui a gagné. En répétant les trois dernières commandes plusieurs fois, vous devriez valider que parfois le joueur gagne, et parfois la banque 🙂

Arrêtons-nous ici pour le test du contrat. Bien évidemment il est possible de faire beaucoup plus, nous verrons cela dans l’article suivant consacré à la mise en place de l’application web qui va se connecter au contrat. Comme vous le verrez ce seront exactement les mêmes concepts, mis à part que grâce à Truffle cela paraîtra plus simple.

J’espère que cette introduction à l’écriture de contrats Ethereum grâce à Solidity vous a donné envie de continuer à suivre cette série d’articles. En attendant la publication du prochain article, n’hésitez pas à m’adresser vos remarques ou questions en commentaire. À bientôt pour la suite de nos aventures !

 

Ressources utiles :

Sébastien Castiel

Développeur web, je m'intéresse énormément au Bitcoin, à la blockchain, et donc tout naturellement à Ethereum. Tip : Ξ 0x7f897a773891a1A20302400170D62e34491A34F5, Ƀ 1HuRJD2MkPkGWUUKx4wnRuNds6CVQrkXyn

10 Réponses

  1. m2p76 dit :

    Bonjour! Super tuto! Parc contre j’ai un souci à l’étape pour importer le module web3 : var Web3 = require(‘web3’). J’ai le message « undefined » ça vous parle..?

    • Bonjour,
      Ce n’est pas une erreur, la valeur retournée par une instruction ‘var …’ est toujours ‘undefined’ 😉
      Si vous tapez ensuite simplement ‘Web3’ vous devriez visualiser le contenu de la variable ‘Web3’.

  2. Shazz dit :

    Tres interessant !
    Par contre moi aussi j’ai un petit souci, depuis la console Truffle, tous les appels a Test RPC plantent:

    truffle(development)> Roulette.new(10, { from: web3.eth.accounts[0], value: web3.toWei(100, « ether ») }).then(function(_contract) { contract = _contract; });
    Error: CONNECTION ERROR: Couldn’t connect to node http://localhost:8545.
    at Object.module.exports.InvalidConnection (/usr/lib/node_modules/truffle/node_modules/web3/lib/web3/errors.js:28:16)

    Alors que le Test RPC tourne bien sur localhost:8545, depuis node,js aucun pb pour y acceder via web3..

    Une idee ?

    • Shazz dit :

      Pour info si Test RPC n’est pas lancé l’erreur est differente :

      truffle(development)> Roulette.new(10, { from: web3.eth.accounts[0], value: web3.toWei(100, « ether ») }).then(function(_contract) { contract = _contract; });
      Error: Invalid JSON RPC response: undefined
      at Object.module.exports.InvalidResponse (/usr/lib/node_modules/truffle/node_modules/web3/lib/web3/errors.js:35:16)

      Pb de version entre truffle et Test RPC ?

  3. Kevin dit :

    Bonjour,

    Je découvre Ethereum et le principe des dApps, le concept est très intéressant.
    Je souhaite savoir comment pouvons-nous monétiser les dApps que nous développons, c’est à dire comment le développeur peut gagner de l’Ether grâce aux utilisateurs qui consomment son dApps ?

    Merci d’avance.
    Cdt,
    Kévin.

  4. bobricard dit :

    De la branlette cette techno…on sait même pas à quoi ça sert !

  5. Sept Cinquante dit :

    super tuto, merci

  6. Hamdi369 dit :

    Bonjour
    Merci pour ce tres clair tutoriel an français
    Bien que totalment novice; je suis tres interessé par les smart-contracts

    j’ai executé toutes les etapes, installé node.js Truffle et TestRPC, cependant je but à l’etape ou je dois me connecter :
    « Créez un nouveau répertoire quelque part sur votre machine, et rendez-y-vous dans votre terminal : mkdir -p ~/Documents/testweb3 ; cd $_ ;
    Installez localement le module web3 pour Node.js : npm install web3 ;
    Lancez l’invite de commande Node.js (node) et importez le module web3 : var Web3 = require(‘web3’). »

    dans le repertoire j’ai bien un dossier « node_modules » et à l’interieur plusieurs autres dossiers dont « web3 » mais dans le command prompt j’ai « >… »

    seriez-vous pourquoi?

    merci pour votre reponse

  7. Redja dit :

    Bonjour,
    Merci beaucoup pour le tuto. Excellent travail.
    Cependant en suivant à la lettre le tuto, j’ai deux erreurs (un à la compilation que j’ai contourné comme un bon débutant en annulant la ligne en question. C’est la ligne « bets[i].player.send(bets[i].value * getPayoutForType(bets[i].betType)); » de la fonction launch.
    le message d’erreur est le suivant:
    « Roulette.sol:71:5: Warning: Return value of low-level calls not used. »

    La deuxième erreur est au moment de l’exécution de la fonction suivante:
    > contract.betEven({ from: web3.eth.accounts[1], value: web3.toWei(1, « ether ») })
    Error: VM Exception while processing transaction: invalid JUMP

    Avez-vous une idée d’où peuvent venir ces erreurs?

    Merci par avance pour votre aide fort précieuse.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

En continuant à utiliser le site, vous acceptez l’utilisation des cookies. Plus d’informations

Les paramètres des cookies sur ce site sont définis sur « accepter les cookies » pour vous offrir la meilleure expérience de navigation possible. Si vous continuez à utiliser ce site sans changer vos paramètres de cookies ou si vous cliquez sur "Accepter" ci-dessous, vous consentez à cela.

Fermer