Drupal 7 et ethereum, un hello world avec web3.js et parity : s’enregistrer sur la blockchain

Cet article suppose que vous connaissez déjà la blockchain (BC). C’est une sorte de base de donnée (coûteuse) infalsifiable, avec, dans le cas d’ethereum (ETH) la possibilité d’exécuter des bouts de code certifiés (dans un langage de programmation proche du javascript : solidity). Si ces notions ne vous sont pas familières, passez votre chemin.

La BC est une technologie en pleine évolution (et en plein boom). Il y aurait beaucoup à dire, mais dans cet article, on va se limiter à ropsten, parity, web3.js 1.0 et … drupal (version 7 – oui c’est un peu vieillot) et comment coder un « hello world » en Drupal pour se connecter à la BC.

Nous allons réaliser 2 choses :

  • connexion d’un user drupal avec un compte de la BC
  • achat d’un token ERC20, avec validation automatique de la transaction (ce qui permet de se passer d’un wallet)

Dans ce premier article, on va déjà s’intéresser au premier cas.

Environnement de développement

A supposer que vous ayez un site Drupal quelconque. Par exemple, un que vous avez installé à l’aide d’ansible. Il va vous falloir en plus le module user_hash, et un champ « ethereum_address » de type string en plus sur votre profil utilisateur. Pour faire fonctionner la BC, pour les développeurs, il est conseillé d’utiliser parity (1.8.3 à l’heure ou j’écris). Une fois installé, vous lancez parity comme ceci :
parity ui --chain=dev --unsafe-expose --jsonrpc-apis=all
Cette ligne de commande permet d’avoir un parity « de dev » qui tourne sans aucune restriction. Quand on démarre et qu’on est dans une VM, j’estime que ce n’est pas encore l’heure de ses pré-occuper de la sécurité. Chaque chose en son temps. La, ce qu’on veut c’est voir la bête tourner.

Si vous souhaitez accéder à l’interface en ligne de parity (et je vous le conseille) : http://192.168.50.5:8180/#/accounts/

192.168.50.5 est l’IP fixe privée que j’ai donné à la VM vagrant (sinon, ça sera probablement sur localhost) :
config.vm.network "private_network", ip: "192.168.50.5
Vous devez obtenir quelque chose comme ceci (ce n’est pas un tuto parity, je n’irais donc pas plus loin à ce sujet) :

A partir de la, il vous faudra créer un compte pour chaque user drupal que vous souhaitez connecter à la BC.

Cette instance de parity fonctionne avec une blockchain de dev, locale, légère, rapide, mais une fois que votre application fonctionnera, il vous faudra basculer sur une version publique de test d’ethereum: par exemple ropsten. L’avantage de parity c’est qu’il le permet très simplement, et qu’il fait aussi office de wallet (porte-feuille ethereum) et aussi il dispose d’outils pour les développeurs (notamment pour les contrats). Notez (mais ce n’est pas le sujet de cet article) que Mist (un autre wallet ethereum) dispose d’une debugger de smart-contract assez bluffant (si jamais vous avez besoin d’en arriver la). Mais en dehors de ce cas, parity fait tout à fait l’affaire.

Préparation des données

[Attention, ce n’est pas un tuto drupal, donc je passe rapidement sur les étapes principales du code Drupal]

On peut communiquer avec le BC de différentes manières possibles, mais le plus simple et le plus répandu c’est d’utiliser la librairie web3.js en javascript, pour se connecter au noeud parity. On pourrait se connecter au noeud en PHP (à l’aide d’ethereum-php mais ce n’est pas une librairie officielle et le coté asynchrone des appels à la BC sont plus facile à gérer en JS).

Récupérez une web3.min.js. Qu’on va ensuite injecter avec un :

drupal_add_js(drupal_get_path('module', 'hellothereum') . '/js/web3.min.js', array('scope' => 'footer'));

De l’autre coté, il va nous falloir un champ pour accueillir l’adresse ethereum du user (à la rigueur on pourrait aussi se contenter de la rechercher le hash du user dans la blockchain pour récupérer son adresse. Il faudrait réfléchir aux conséquences en terme de performance et de sécurité des 2 choix, mais ça dépasse le cadre de ce simple article qui vise à découvrir les fonctions de base).

Cette partie se fait avec un field_create_field et un field_create_instance dans le hook_enable :

  if (!field_info_field('field_ethaddress')) {
    $field = array(
        'field_name' => 'field_ethaddress', 
        'type' => 'text', 
    );
    field_create_field($field);

    // Create the instance on the bundle.
    $instance = array(
        'field_name' => 'field_ethaddress', 
        'entity_type' => 'user', 
        'label' => 'Ethereum address', 
        'bundle' => 'user', 
        'settings' => array(
           // Here you inform either or not you want this field showing up on the registration form.
            'user_register_form' => 1,
        ),
        'widget' => array(
            'type' => 'textfield',
            'weight' => '1',
        ), 
    );
    field_create_instance($instance);
}

Avec le module user_hash, et le champ ethaddress, on est équipé pour aller taquiner la blockchain. Il ne nous reste plus qu’à envoyer les informations au javascript qui va passer par Web3 pour appeler les smartcontract. Les adresses des smartcontract et leur ABI sont stockés dans la table variable et envoyées au JS :

    drupal_add_js(array(
      'ethereum_user' => array(
        'contract' => array(
          'address' => variable_get('ethereum_user_register_drupal_deployed_contract_address'),
          'abi' => variable_get('ethereum_user_register_drupal_deployed_contract_abi'),
        ),
        'fallback_node' => variable_get('ethereum_user_register_drupal_fallback_node'),
        'token' => variable_get('ethereum_user_registry_list_token'),
        'user' => array(
          'hash' => $this_user->hash,
          'address' => $this_user_ethereum_address,
        ),
      ),
    ), 'setting');

Appel des smart-contracts en JS

On récupère d’abord toutes les infos nécessaires envoyées par Drupal via Drupal.settings :

        window.web3 = new Web3(new Web3.providers.HttpProvider(Drupal.settings.ethereum_user.fallback_node));
        var user_address = Drupal.settings.ethereum_user.user.address.toLowerCase();
        var contract_abi = JSON.parse(Drupal.settings.ethereum_user.contract.abi);
        var contract_address = Drupal.settings.ethereum_user.contract.address;
        var contract = new web3.eth.Contract(contract_abi, contract_address);
        var user_hash = Drupal.settings.ethereum_user.user.hash;

A partir de la, on va pouvoir enfin faire notre premier appel de smart-contract. Attention, c’est du code asynchrone qui utilises des « promises » ou des « callbacks » (au choix). On n’écrit donc pas du code séquentiel (comme c’était le cas avec les premières versions de web3.js). Dans les versions précédentes de web3.js on faisait un appel de méthode classique. Cette fois il faut rajouter .methods avant l’appel et .call après. Ce qui donne :

contract.methods.validateUserByHash(user_hash).call({from: user_address}, function (error, result) {

Voyons le code du contrat correspondant :

  function validateUserByHash (bytes32 drupalUserHash) constant returns (address result){
      return _accounts[drupalUserHash];
}

Il est très simple. Le contrat dispose d’une table de correspondance, comme un tableau associatif en php, et il se contente de renvoyer une entrée à l’indice indiqué, si elle existe.

Comme vous le voyez, le contrat ne définit qu’un seul paramètre qu’on retrouve dans le .validateUserByHash(user_hash), le reste de l’appel, contient les paramètres « génériques » d’un appel de contrat : .call({from: user_address}, function (error, result) { qui est le user qui fait l’appel de contrat, et quelle est la callback à déclencher en retour. Dans le cas des transactions payantes on pourrait aussi ficher un prix.

Le traitement du résultat de l’appel se fait donc dans la fonction de callback. S’il n’y a pas eu d’erreur, c’est la qu’on peut vérifier si l’utilisateur est déjà enregistré et, si c’est pas le cas, lui proposer une transaction d’enregistrement :

contract.methods.newUser(user_hash).send({from: user_address})

Cette fois-ci c’est un .send à la place du .call, j’y reviens un peu plus loin. Voici le code solidity correspondant :

  function newUser(bytes32 drupalUserHash) public {

    if (_accounts[drupalUserHash] == msg.sender) {
      // Hash allready registered to address.
      accountCreated(msg.sender, drupalUserHash, 4);
    }
    else if (_accounts[drupalUserHash] > 0) {
      // Hash allready registered to different address.
      accountCreated(msg.sender, drupalUserHash, 3);
    }
     else if (drupalUserHash.length > 32) {
      // Hash too long
      accountCreated(msg.sender, drupalUserHash, 2);
    }
    else if (_registrationDisabled){
      // Registry is disabled because a newer version is available
      accountCreated(msg.sender, drupalUserHash, 1);
    }
    else {
      _accounts[drupalUserHash] = msg.sender;
      accountCreated(msg.sender, drupalUserHash, 0);
    }
}

Le code est très simple en soi si on enlève les vérifications qui sécurisent le contrat : _accounts[drupalUserHash] = msg.sender;. La dernière ligne est un évènement, ce sera traité dans les prochains articles.

Cette fois, par contre, on va utiliser les « promises » pour réagir à la transaction plutôt qu’une callback. Dans ce cas précis les promisses permettent d’avoir une action à différents moments. Notez une différence majeure par rapport au .call précédent : elle doit être validée par l’utilisateur, dans son wallet, puis être ensuite « minée », c’est à dire confirmée dans la blockchain. Typiquement, on est dans de l’asynchrone la. Notre javascript, ni notre site, ne s’arrêtent de vivre parce que la blockchain fait son boulot de son coté.

              .on('transactionHash', function (transactionHash) {
                $('#ethereum_user_registry_sign').html('<p>Please wait between 10 seconds and a few minutes for the transaction to be mined on the Ethereum network. You can reload this page at any time to see if the transaction is confirmed. Or you can <a href="https://etherscan.io/tx/' + transactionHash + '" target="_blank">see the transaction status in live</a>.</p>');
              })
              .on('error', function (error) {
                // 0 is "success error" in RegisterDrupal.sol
                if (error != 0) {
                  console.error;
                }
              })
              .on('confirmation', function (confirmationNumber, receipt) {
                // Should be 0 because there's no block after this transaction yet.
                console.log('Number of confirmation blocks: ' + confirmationNumber);
               })

On peut réagir à plusieurs évènements. TransactionHash : c’est quand la transaction est crée, c’est à dire, quand l’utilisateur valide la transaction dans son wallet. Sinon, on déboule sur une erreur. Si la transaction est validée, en principe, elle va finir en confirmation, sauf s’il y a une error déclenchée dans le smart-contract. Si votre smartcontract est bien fait, il va vous remonter une erreur explicite. Sinon … c’est la qu’on commence à pleurer ! Mais il y a une solution.

Si jamais vous être coincés et que ne comprenez pas pourquoi votre smartcontract ne renvoies pas le résultat attendu, vous pouvez lancer Mist qui contient un debugger de smart-contract, tout à fait bluffant. Ce n’est pas le sujet de cet article, je ne m’étendrais pas, mais sachez simplement qu’il permet de dérouler votre code comme film, y compris de faire du rewind et qu’il est vraiment très bien fait. C’est le même debugger que « Remix IDE« , mais qui tourne sur votre BC locale.

Voila, vous avez les bases. Il nous restera à voir dans les prochains articles : les transactions payantes, les évènements, l’auto-signature, pour faire un premier tout d’horizon plus complet des possibilités principales de la blockchain.

Laisser un commentaire

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