Drupal 7 et ethereum, un hello world avec web3.js et parity : acheter un token

A supposer que vous soyez déjà enregistré en tant que user drupal reconnu sur la blockchain et que vous ayez déployé un contrat Token ERC20, nous allons voir comment faire pour :

  • afficher votre solde de token
  • acheter un token avec vos ethereums
  • modifier votre solde dès qu’ils change
  • valider la transaction automatiquement (2 méthodes)

Le code complet (et testé) est disponible sur github. Je ne reviens pas sur les principes de base : Drupal.settings, connexion à la blockchain, création de l’instance du contract dans JS. On va se concentrer sur les manipulations que l’on fait sur le contrat, en javascript, via web3.js.

Afficher votre solde en temps réel

A supposer que vous ayez initialisé votre contrat, afficher le solde en token de votre compte se fait avec un simple .call :

token_contract.methods.balanceOf(clientAddress).call().then(function(result){$("#client-token").html(result);});

Mais cet affichage sera fait une seule fois au chargement de la page. Hors, la blockchain peut enregistrer des transactions depuis n’importe quel client. Le solde peut donc changer à tout moment. Comment faire pour que votre soit mise à jour en temps réel le cas échéant ? Si votre smartcontract l’a prévu, vous pouvez utiliser les évènements à cet effet :

event Transfer(address indexed from, address indexed to, uint256 value);

    function _transfer(address _from, address _to, uint _value) internal {
        // Prevent transfer to 0x0 address. Use burn() instead
        require(_to != 0x0);
        // Check if the sender has enough
        require(balanceOf[_from] >= _value);
        // Check for overflows
        require(balanceOf[_to] + _value > balanceOf[_to]);
        // Save this for an assertion in the future
        uint previousBalances = balanceOf[_from] + balanceOf[_to];
        // Subtract from the sender
        balanceOf[_from] -= _value;
        // Add the same to the recipient
        balanceOf[_to] += _value;
        Transfer(_from, _to, _value);
        // Asserts are used to use static analysis to find bugs in your code. They should never fail
        assert(balanceOf[_from] + balanceOf[_to] == previousBalances);
}

Le smartcontract définit un évènement Transfer qui est appelé dans la fonction interne _transfer. Il déclenche un appel qui peut être intercepté coté javascript à l’aide du code suivant :

token_contract.events.Transfer().on('data', function(event){
  token_contract.methods.balanceOf(clientAddress).call().then(function(result){$("#client-token").html(result);});
});

On retrouve le même code d’affichage du solde, mais en tant résultat d’une promise d’un .events.Transfer().on('data', {}) . Sauf que si vous vous arrêtez la, vous allez avoir un petit problème. Le HttpProvider que l’on utilises pour web3 ne supporte pas les évènements. Il faut utiliser le WebsocketProvider :

window.web3 = new Web3(new Web3.providers.WebsocketProvider(fallback));

Et pour que ça fonctionne on doit saisir une url du style : ws://localhost:8546. Le port n’est plus 8545 mais 8546 et le http transformé en ws. A partir de ce moment la, votre appel d’évènement fonctionnera et le solde sera mis à jour en temps réel.

Acheter des tokens

L’achat de Token doit être prévu dans votre smartcontract. C’est une simple fonction qui augmente votre solde à une nuance près, elle est « payable » :

    function buy() payable public {
        uint amount = (msg.value / buyPrice) * unit;      // calculates the amount
        _transfer(this, msg.sender, amount);              // makes the transfers
}

L’appel se fait donc simplement par un .send :

token_contract.methods.buy().send({from:clientAddress, value:web3.utils.toWei(0.001, "ether")})

Notez que dans le cas présent (un token ERC20 donc) le solde n’est pas simplement augmenté, mais un transfert est réalisé depuis le contrat lui même vers le user. C’est la signification du _transfer(this, Ce qui peut sembler étrange au premier abord, mais qui permet en fait de fixer le nombre de Token à la création (selon la même astuce déflationniste utilisée par bitcoin qui consiste à ne créer que 21 millions de bitcoin au maxium). En fonction du prix d’achat du token et du taux de conversion, vos ethers seront convertis en token.

Valider la transaction via parity

Avec le code tel qu’il est présenté précédemment, l’utilisateur devra obligatoirement basculer sur son wallet (Mist, Partiy, Jaxx, MetaMask ou autre) pour valider la transaction en saisissant son mot de passe. Pour un utilisateur expérimenté habitué à ethereum c’est acceptable, mais pour le grand public, il y a un risque que l’utilisateur attende sans comprendre qu’il doit valider sa transaction « de l’autre coté ». Mais nous pouvons l’aider. Il y a un 2ème cas ou la validation de transaction peut-être intéressante : pour les développeurs. C’est en effet pénible de devoir aller valider à chaque fois qu’on teste. Enfin, c’est carrément rédhibitoire pour les tests automatisés. Dans tous ces cas, il peut être utile de savoir comment faire.

La première méthode consiste à appeler parity nous même pour lui dire de valider la dernière transaction de son pipe. Ce n’est pas la méthode la plus sûre dans la mesure ou quelqu’un d’autre pourrait avoir rajouté une transaction dans l’intervalle. Mais ça peut suffire (notamment pour les développeurs).

Nous allons utiliser l’API JsonRPC de parity, qui s’utilise comme un appel AJAX. Cela se fait en 2 étapes, d’abord signer_requestsToConfirm pour récupérer les transactions en attente de signature, puis, nous allons demander à l’utilisateur son mot de passe et l’envoyer à parity avec un signer_confirmRequest.  Ce qui nous donne le code suivant :

        autoSign = function() {
          $.ajax({
            type:"POST",  url: fallback, Accept : "application/json", contentType: "application/json",  dataType: "json",
            data: JSON.stringify({"method":"signer_requestsToConfirm","params":[],"id":1,"jsonrpc":"2.0"}),
            success: function(result) { 
              if (result.result == []) alter('Could not sign');
              if (result.result[0] == undefined) alter('Could not sign');
              id = result.result[0].id;
              pass = $('#eth-password').val();
              $.ajax({
                type:"POST", url: fallback, Accept : "application/json", contentType: "application/json", dataType: "json",
                data: JSON.stringify({"method":"signer_confirmRequest","params":[id, {}, pass],"id":1,"jsonrpc":"2.0"}),
                success: function(result) { alert('transaction validated automatically'); }
              });
            }
          });
}

Mais ce n’est pas une solution entièrement satisfaisante dans la mesure ou : on passe par parity. Que faire si on souhaite utiliser geth ? Et s’il y a d’autres transactions dans le pipe ? Sans parler du fait qu’il est difficile de savoir « quand » faire l’appel vers parity (il faut lui laisser le temps de créer sa transaction). Vous aurez remarqué dans le code que l’appel se fait après un delai :

            setTimeout(function() {autoSign();}, 1000);

Nous allons maintenant voir comment signer la transaction proprement avec eth3. Mais c’est un peu plus compliqué (et surtout non documenté à l’heure ou j’écris).

Valider la transaction avec web3

Sans plus tarder voici le code « propre » d’un appel de transaction signé dans web3 :

        autoSignWeb3 = function (pass, onreceipt) {
          var walletContractAddress = Drupal.settings.blockchain.token_deployed_contract_address_fallback;
          var privateKey = new buffer.Buffer(pass, 'hex');
          var fromAccount = clientAddress;
          var signature = _.find(JSON.parse(Drupal.settings.blockchain.token_deployed_contract_ABI), { name: 'buy' });
          var payloadData = web3.eth.abi.encodeFunctionCall(signature, []);
          gasPrice = web3.eth.gasPrice;
          gasPriceHex = web3.utils.toHex(gasPrice);
          gasLimitHex = web3.utils.toHex(300000);
          web3.eth.getTransactionCount(fromAccount).then((nonce) => {
            nonceHex = web3.utils.toHex(nonce);
            var rawTx = {
              nonce: nonceHex,
              gasPrice: gasPriceHex,
              gasLimit: gasLimitHex,
              to: walletContractAddress,
              from: fromAccount,
              value: web3.utils.toHex(web3.utils.toWei(0.001, "ether")),
              data: payloadData
              };
            var tx = new ethereumjs.Tx(rawTx);
            tx.sign(privateKey);
            var serializedTx = tx.serialize();
            web3.eth.sendSignedTransaction('0x' + serializedTx.toString('hex')).then( () =>{ onreceipt(); return null; } );
            return null;
          });
}

Si vous regardez la documentation de web3, vous trouverez facilement cette fonction web3.eth.sendSignedTransaction qui permet d’envoyer des transactions signées. Mais le problème c’est qu’on doit envoyer une chaine représentant la transaction. Il faut donc construire cette chaine. On est loin du .methods.xxx.send() plutôt naturel. Construire une sorte de code hexadécimal pour faire appel … ce n’est pas si simple, surtout que les exemples sur le net concernent des versions différentes ou plus anciennes de web3.js.

Pour y parvenir je me suis inspiré du code trouvé ici. Sauf que sendRawTransaction est remplacé par sendSignedTransaction, mais surtout :

var solidityFunction = new SolidityFunction('', _.find(ABI, { name: 'sendCoin' }), '');
var payloadData = solidityFunction.toPayload([toAccount, 3]).data;

est remplacé par

 var payloadData = web3.eth.abi.encodeFunctionCall(signature, [toAccount, 1]);

C’est cette partie qui fût la plus compliquée à trouver dans le dédalle du net. On doit faire appel à 3 libraires js externes :

  • ehtereumjs-tx qui sert à sérialiser la transaction.
  • buffer qui sert à envoyer la clé à la transaction
  • lodash qui sert à récupérer la signature de la méthode qu’on veut appeler dans l’ABI

De plus, l’utilisateur ne doit pas saisir le mot de passe de son wallet parity mais sa clé privée. On pourrait tout à fait crypter sa clé à l’aide d’un mot de passe et ne lui demander que ce mot de passe (au final, c’est ce que fait parity), mais ça dépasse le cadre de cet article.

Laisser un commentaire

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