Il y a un peu plus d’un an à présent, nous avons développé une solution d’horodatage basée sur une blockchain. Elle permet de prouver l’antériorité d’un contenu (une idée ou une création par exemple) via l’utilisation d’une blockchain publique Ethereum directement depuis son navigateur. Donc sans organe central de contrôle. Il s’agit d’un cas d’usage simple mais parfaitement applicable aux besoins métiers des applications de nos clients : sceller un contenu et ses métadonnées de manière sûre et pérenne. Le code source est publié sous la licence Apache 2.
Pour faciliter la lecture, nous vous proposons une série de cinq articles qui présentent trois points de vue complémentaires de l’outil :
- Point de vue conceptuel : 🎨 Comprendre les notions de base d’une blockchain publique
- Point de vue utilisateur : 🕺 Interagir avec l’application décentralisée
- Point de vue développeur : 🏠Configurer son environnement de développement
- Point de vue développeur : 🤝 Comprendre le développement du smart contract
- Point de vue développeur : 💻 Comprendre le développement du client
Ce cinquième et dernier article est l’occasion d’aborder le développement du client web qui interagit avec le smart contract développé et déployé précédemment.
Les sources sont disponibles dans le sous-projet front. Il a principalement été écrit avec VueJS pour la présentation et web3 pour les appels à la blockchain.
Nous nous intéressons ici uniquement aux interactions avec la blockchain. Le code est simplifié dans ce qui suit pour se focaliser sur ce qui est essentiel.
# Module blockchain (web3 / jssha)
Ce qui concerne purement la blockchain a été placé dans un module dédié placé dans front/src/lib/BlckchnAntProver.js.
web3.js est un ensemble de librairies qui permet d’interagir avec un nœud Ethereum local ou distant via HTTP, IPC ou WebSocket. On l’importe :
1 |
import Web3 from 'web3' |
Nous importons également les métadonnées produites lors de la compilation du smart contract AntProver.sol (cf. article précédent) afin de référencer le contrat ABI et de récupérer les adresses du smart contract déployé dans différents réseaux :
1 2 3 |
import compiledBlckchnAntProver from '../../../smart-contracts/build/contracts/AntProver.json' const contractAbi = compiledBlckchnAntProver.abi const networks = compiledBlckchnAntProver.networks |
L’identifiant du réseau choisi par l’utilisateur dans Metamask est récupéré via web3 :
1 2 3 4 5 |
const getNetworkId = () => { return web3 && web3.currentProvider ? web3.currentProvider.networkVersion || web3.currentProvider._network : null } |
L’initialisation est réalisée par un appel à la fonction init qui prend deux paramètres :
allowGanache
: pour permettre un fallback Ganache. Par exemple on l’interdit en production avec le testprocess.env.NODE_ENV !== 'production'
_hashAddedCallback
: un callback optionnel pour réagir lorsqu’un événement arrive
Pour l’initialisation, on crée une instance de Web3 en utilisant par ordre de préférence :
window.ethereum
injecté par Metamask (dernière API)window.web3.currentProvider
également injecté par Metamask dans ses versions plus anciennesWeb3.providers.HttpProvider
: un provider HTTP pour se connecter à Ganache en fallback si on l’a permis
Les commentaires indiquent les étapes d’initialisation :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
const init = async (allowGanache, _hashAddedCallback) => { hashAddedCallback = _hashAddedCallback if (window.ethereum) { // Navigateur Dapp moderne web3 = new Web3(window.ethereum); try { // Demande d’accès si nécessaire (fenêtre Metamask) await window.ethereum.enable(); // Les données des comptes sont à présent accessibles initContract() // Ecoute des changements de réseaux éventuels (action de l’utilisation dans Metamask) window.ethereum.autoRefreshOnNetworkChange = false window.ethereum.on('networkChanged', initContract) } catch (error) { // L’utilisateur a empêché l’accès au compte ou le réseau n’est pas accessible // Signifier l’erreur } } else if (window.web3) { // Ancien navigateur Dapp web3 = new Web3(web3.currentProvider); // Les comptes sont toujours exposés web3.eth.getAccounts().then(console.log) initContract() } else { // Navigateur non Dapp (ex : extension Metamask non installée). // On permet malgré tout l’activation de Ganache car le provider est différent (http). if (allowGanache) { web3 = new Web3(new Web3.providers.HttpProvider('http://127.0.0.1:7545')); initContract() } else { // Signifier que l’extension Metamask est indispensable } } } |
L’initialisation (ou la rĂ©initialisation) de l’accès au smart contract est rĂ©alisĂ©e dans la fonction initContract
. Elle consiste essentiellement à créer une instance de web3.eth.Contract
et relayer l’événement HashAdded
via le callback de manière à faire réagir l’application (pour rappel, les événements entrants sont écoutés afin de générer les certificats pdf au besoin) :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const initContract = () => { // Nettoyage éventuel des souscriptions aux événements if (hashAddedSubscription) { web3.eth.clearSubscriptions() } // Identifiant de réseau 5777 par défaut (cas dans d'une navigation privée avec Ganache) contractAddress = networks[getNetworkId() || 5777].address contract = new web3.eth.Contract(contractAbi, contractAddress) // Souscriptions aux événements hashAddedSubscription = contract.events.HashAdded((error, t) => { if (!hashAddedCallback) { return } // [simplifié pour la lecture] Gestion de l’événement via un callback avec les informations de la transaction t : // les métadonnées t.address, t.transactionHash // les données t.returnValues.hash, t.returnValues.comment et t.returnValues.from let data = { /* ... */ } hashAddedCallback(data) }); } |
Pour le référencement d’un contenu, on utilise directement la fonction importée des métadonnées du smart contract en traitant la réponse avec un callback après s’être assuré d’avoir bien le compte utilisateur (ici, on prend le premier) :
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const sendHash = (hash, comment, callback) => { web3.eth.getAccounts((error, accounts) => { contract.methods.addDocHash(hash, comment).send({ from: accounts[0] }, (error, tx) => { if (error) { callback(error, null) } else { callback(null, tx) } }) }) } |
Les fonctions de lecture de données répondent au même principe. Par exemple, la recherche d’un contenu prend un hash en entrée et appelle un callback pour traiter le retour :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
const findHash = (hash, callback) => { if (!(web3.utils.isHexStrict(hash) && web3.utils.hexToBytes(hash).length == 32)) { callback('Hashcode non valide', null) return } contract.methods.findDocHash(hash).call((error, result) => { if (error) { callback(error, null) } else { // On récupère les résultats dans l'ordre let i = 0 let mineTime = new Date(result[i++] * 1000) let block = result[i++] let sender = result[i++] let comment = result[i++] let resultObj = { hashcode: hash, comment, sender, block, mineTime } callback(null, resultObj) } }); } |
L’écriture de quelques fonctions de manipulation de hash a été nécessaire : readUploadedFileAsText
, calculateHashFromBytes
et calculateHashFromFile
. Ces dernières ne sont pas détaillées dans cet article. Je vous invite à consulter la librairie jssha utilisée en background.
Pour finir sur ce module, les fonctions sont exportées afin d’être utilisées par l’application. L’ensemble des fonctions est le suivant :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
export default { // Accès au smart contract (écriture / lecture) findHash, sendHash, size, list, listBySender, // Initialisation et informations diverses liées à la blockchain init, getCurrentProvider, getNetworkId, getContractAddress, getAccounts, getTransactionReceipt, getTransaction, etherscanBaseUrl, // Utilitaires de calcul d’empreintes calculateHashFromFile, calculateHashFromBytes } |
Ainsi ce module permet de traiter l’aspect purement lié à la blockchain : initialiser la connexion au smart contract déployé dans la blockchain sélectionnée par l’utilisateur dans Metamask, s’assurer que le compte de l’utilisateur est sélectionné avant de tenter une écriture dans la blockchain (ou provoquer sa sélection), se charger des appel à la blockchain et mettre en place l’écoute des événements afin de les relayer.
La suite est moins détaillée dans la mesure où il s’agit d’utiliser les fonctions de ce module.
# Initialisation
L’application est constituée de bloc fonctionnels et techniques. Chacun des blocs est un composant Vue.js créé dans l’application App.vue. On retrouve logiquement l’inclusion des balises dans le template de l’application :
1 2 3 4 5 |
<Upload class="tile"/> <Events class="tile"/> <Check class="tile clear-left"/> <List class="tile"/> <Status class="tile clear-left"/> |
Ainsi que l’initialisation évoquée plus haut réalisée lorsque l’application Vue.js est montée et que la page est chargée.
On initialise le lien avec la blockchain et on stocke les informations dans le store Vuex qui permet la gestion d’états dans l’application. Ce dernier est utilisé à la fois pour les événements entrants et pour les informations techniques sur la blockchain choisie :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
mounted(){ window.addEventListener('load', async () => { // Initialisation du lien avec le smart contract // * fallbak vers Ganache activé sauf pour la production // * callback sur un événement : ajout de l’événement dans le store await BlckchnAntProver.init(process.env.NODE_ENV !== 'production', eventData => { this.$store.dispatch('addNewEvent', eventData) }) // On récupère les informations techniques dans le store let currentProvider = BlckchnAntProver.getCurrentProvider() if (currentProvider) { this.$store.dispatch('setProvider', currentProvider) this.$store.dispatch('setContractAddress', BlckchnAntProver.getContractAddress()) } }) } |
# Composant d’écriture
Le bloc J’ancre un contenu est implĂ©mentĂ© par le composant Upload.vue.
Le module blockchain décrit précédemment permet d’interagir avec la blockchain sans connaître les détails sous-jacents. On utilise la fonction de calcul du hash lorsqu’un fichier est chargé :
1 2 3 4 5 |
async fileChosen(evt) { // ... this.form.hashcode = await BlckchnAntProver.calculateHashFromFile(theFile) // ... } |
Ainsi que la fonction d’envoi d’un hash lorsque le formulaire est soumis :
1 2 3 4 5 6 7 8 9 10 |
onSubmit(evt) { // ... BlckchnAntProver.sendHash(this.form.hashcode, this.form.comment, (error, transaction) => { if (error) { this.error = error return } this.transactionResult = transaction }) } |
# Composant de lecture
La recherche du référencement d’un fichier du bloc Je vérifie un contenu est réalisée dans le composant Check.vue.
Là aussi, le composant est agnostique vis à vis de la blockchain. On calcul le hash lorsqu’un fichier est chargé :
1 2 3 4 5 |
async fileChosen(evt) { // ... this.form.hashcode = await BlckchnAntProver.calculateHashFromFile(evt.target.files[0]) // ... } |
Et on effectue la recherche lorsque le formulaire est soumis :
1 2 3 4 5 6 7 8 9 10 11 |
onSubmit(evt) { // ... BlckchnAntProver.findHash(this.form.hashcode, (error, result) => { if (error) { this.error = error } else { this.info = result } }) } |
# Smart contract et ABI
Le contrat ABI (Application Binary Interface) définit la manière d’interagir avec un smart contract. Il s’agit d’une interface entre le smart contract et un client.
Nous avons vu au début de cet article que nous avons importé le contrat ABI du smart contract que nous avons développé initialement puisque nous le versionnons dans les sources :
1 2 |
import compiledBlckchnAntProver from '../../../smart-contracts/build/contracts/AntProver.json' const contractAbi = compiledBlckchnAntProver.abi |
Nous aurions également pu le récupérer à partir de la blockchain. Par exemple, le smart contract est déployé dans la blockchain Goerli à l’adresse 0xE9D0E1857CcaAE1E608E6e9b5fdFa1CD6D267733 et son code source est vérifié. Le contrat ABI est disponible au format json à cette adresse.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
const contractAbi = [ /*...*/ { "constant": true, "inputs": [ { "name": "hash", "type": "bytes32" } ], "name": "findDocHash", "outputs": [ { "name": "", "type": "uint256" }, { "name": "", "type": "uint256" }, { "name": "", "type": "address" }, { "name": "", "type": "string" } ], "payable": false, "stateMutability": "view", "type": "function" }, /*...*/ ] |
# Synthèse de l’article
Cet article illustre le développement d’un client web Ethereum.
Il faut retenir que l’extension Metamask expose son API en injectant l’objet ethereum dans la page que nous fournissons lors de l’initialisation de l’application avec web3, librairie de communication avec les blockchains Ethereum.
Pour simplifier l’interaction avec un smart contract déployé dans une blockchain Ethereum, il est conseillé de commencer par écrire une API dédiée dans un module afin de traiter l’initialisation, la sélection de compte, les appels concrets et relayer les événements.
Dans le reste de l’application, il s’agit ainsi d’utiliser cette API qui rend la consommation agnostique et facilite l’écriture du code, sa compréhension et sa maintenance.
Nous pouvons exploiter le contrat ABI généré lors de la compilation du smart contract ou le récupérer lorsque ce dernier est déployé. Il fournit déjà un premier niveau d’API avec les objets métiers du smart contract.
# Synthèse de la série d’articles
Cette série d’articles touche à sa fin.
Le cas d’usage se prête bien à l’exercice pédagogique : faire de la preuve d’antériorité à partir d’un smart contract déployé dans une blockchain, directement depuis son navigateur.
Nous avons commencé avec un point de vue conceptuel dans le premier article en expliquant ce qu’apporte la blockchain dans le cas d’usage : notamment la simplicité administrative, le coût d’infrastructure quasi nul et la résilience. Nous avons également synthétisé le fonctionnement d’une blockchain Ethereum : la sécurité assurée par la crypto et les consensus, l’organisation du stockage en chaîne de blocs distribuée, le coût des transactions et les jetons de monnaie et ce qu’apportent les smarts contracts. La suite du premier article a été consacrée à la manière d’interagir avec une blockchain depuis son navigateur : l’extension Metamask et son portefeuille, comment l’alimenter pour réaliser des micro-paiements. Puis nous avons montré en quoi une blockchain publique est transparente : les opérations sont visibles ainsi que les pseudonymes de leurs auteurs. Les transactions d’une blockchain ne sont donc pas forcément anonymes.
Dans un second temps, nous avons exposé un point de vue utilisateur dans le deuxième article avec la démonstration de l’ergonomie de l’application : la configuration initiale de Metamask, puis l’utilisation de l’outil qui reste classique hormis lors d’une opération d’écriture. En effet, le consentement de l’utilisateur est demandé dans la mesure où ce dernier supporte le coût de la transaction. En revanche, aucun frais n’est appliqué pour la consultation.
La suite est consacrée au point de vue du développeur en trois temps : la configuration de son environnement de développement, le développement du smart contract et le développement du client web.
Les sources du projet sont accessibles dans la mesure où elles sont publiées sous la licence Apache 2. Pour les développements, il est possible d’instancier une blockchain de développements avec Ganache. C’est cette blockchain qui est exploitée dans l’article 3 qui décrit comment compiler et le déployer le smart contract dans celle-ci et comment servir le client web pour commencer à valider son environnement.
Le développement du smart contract est expliqué dans l’article 4 : le langage solidity, le squelette, les structures et les variables, les fonctions d’écriture et de lecture, les prérequis ainsi que la production d’événements. Pour compléter l’article 3, nous précisons la manière de gérer les migrations grâce à truffle et le déploiement vers une blockchain publique officielle à travers Infura.
Enfin, le dernier article expose une manière de produire le client web : l’écriture d’un module javascript dédié aux interactions avec la blockchain via Metamask à travers web3 et l’utilisation de cette API dans le reste de l’application qui constitue un projet classique dans la manière de l’aborder, l’ergonomie liée à la configuration de son compte et le consentement mis à part.
Je vous remercie d’avoir pris le temps de nous lire et vous invite Ă nous laisser un commentaire Ă la suite de cet article si vous avez des questions ou des remarques ou simplement pour nous indiquer si vous ĂŞtes arrivĂ©s jusque-lĂ sain et sauf ! ⬎
20 novembre 2020 at 18 h 36 min
Je vous remets l’ensemble des liens de la sĂ©rie :
• 🎨 Comprendre les notions de base d’une blockchain publique
 ↳ https://blog.atolcd.com/solution-blockchain-horodatage-1-concepts/
• 🕺 Interagir avec l’application décentralisée
 ↳ https://blog.atolcd.com/solution-blockchain-horodatage-2-utilisation/
• 🏠Configurer son environnement de développement
 ↳ https://blog.atolcd.com/solution-blockchain-horodatage-3-dev-environnement/
• 🤝 Comprendre le développement du smart contract
 ↳ https://blog.atolcd.com/solution-blockchain-horodatage-4-dev-smart-contract/
• 💻 Comprendre le développement du client
 ↳ https://blog.atolcd.com/solution-blockchain-horodatage-5-dev-client-web/
• 🚄 Synthèse globale
 ↳ https://blog.atolcd.com/solution-blockchain-horodatage-5-dev-client-web/#synthese-de-la-serie-darticles