Fermer

juillet 24, 2018

Création d'une interface utilisateur Web3 pour un contrat DAO –


Dans partie 6 de cette série de didacticiels sur la construction de DApps avec Ethereum, nous avons complété le DAO en ajoutant vote, blacklisting / unblacklisting, distribution et retrait de dividendes, tout en ajoutant des fonctions supplémentaires pour bonne mesure. Dans ce tutoriel, nous allons créer une interface web pour interagir avec notre histoire, car nous ne pouvons pas compter sur un engagement de l'utilisateur. Donc, c'est la dernière partie de notre histoire avant de la lancer dans la nature.

Puisque ce n'est pas un tutoriel d'application web, nous allons garder les choses extrêmement simples. Le code ci-dessous n'est pas prêt pour la production, et est destiné à servir uniquement de preuve de concept sur la façon de connecter JavaScript à la blockchain. Mais d'abord, ajoutons une nouvelle migration

Automatiser les transferts

Maintenant que nous déployons notre jeton et DAO, ils s'assoient sur la blockchain mais n'interagissent pas. Pour tester ce que nous avons construit, nous devons transférer manuellement la propriété du jeton et équilibrer le DAO, ce qui peut être fastidieux pendant les tests.

Écrivons une nouvelle migration qui fait cela pour nous. Créez le fichier 4_configure_relationship.js et ajoutez le contenu suivant:

 var Migrations = artefacts.require ("./ Migrations.sol");
var StoryDao = artefacts.require ("./ StoryDao.sol");
var TNSToken = artefacts.require ("./ TNSToken.sol");

var storyInstance, tokenInstance;

module.exports = fonction (déployeur, réseau, comptes) {

    deployer.then (function () {
            renvoie TNSToken.deployed ();
        }). then (function (tIns) {
            tokenInstance = tIns;
            return StoryDao.deployed ();
        }). then (function (sIns) {
            storyInstance = sIns;
            return balance = tokenInstance.totalSupply ();
        }). then (function (bal) {
            retourner tokenInstance.transfer (storyInstance.address, bal);
        })
        .then (fonction (quelque chose) {
            retourner tokenInstance.transferOwnership (storyInstance.address);
        });
}

Voici ce que fait ce code. D'abord, vous remarquerez que c'est basé sur la promesse. Il est plein de puis de appels. C'est parce que nous dépendons d'une fonction retournant des données avant d'appeler la suivante. Tous les appels de contrat sont basés sur des promesses, ce qui signifie qu'ils ne renvoient pas de données immédiatement parce que Truffle doit demander des informations au nœud, ainsi une promesse de retourner des données à un moment ultérieur est faite. Nous forçons le code à attendre ces données en utilisant puis et en fournissant tous les appels puis avec des fonctions de rappel qui sont appelées avec ce résultat quand il est finalement donné.

:

  • d'abord, demander au noeud l'adresse du jeton déployé et le renvoyer
  • puis, en acceptant cette donnée, l'enregistrer dans une variable globale et demander l'adresse du DAO déployé et la renvoyer
  • en acceptant ces données, en les sauvegardant dans une variable globale et en demandant le solde que le propriétaire du contrat de jetons aura dans son compte, ce qui est techniquement l'offre totale, et renvoyez ces données
  • puis, une fois que vous obtenez cet équilibre, utilisez-le pour appeler la fonction transfert de ce jeton et envoyez des jetons à l'adresse DAO et renvoyez le résultat
  • puis, ignorez le résultat retourné – nous voulions juste savoir quand c'est fait – et finalement transférer la propriété du jeton à l'adresse du DAO, renvoyant les données

En cours d'exécution truffle migrate --reset devrait maintenant produire une sortie comme celle-ci:

 Truffle migrate

The Front End

Le début est un une page HTML régulière et statique avec du JavaScript pour communiquer avec la blockchain et du CSS pour rendre les choses moins moche.

Créons un fichier index.html dans le sous-dossier public et lui donner le contenu suivant:





     L'histoire sans fin 
    
    
    
    



    

L'histoire sans fin

Une histoire sur la blockchain Ethereum, communauté organisée et modérée à travers une Organisation Autonome Décentralisée (DAO)

Chapitre 0

C'est une nuit pluvieuse dans le centre de Londres.


Ceci est un exemple de soumission. Une proposition pour sa suppression a été soumise.
0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9
Ceci est une présentation longue. Il a plus de 244 caractères, juste nous pouvons voir à quoi il ressemble lorsqu'il est rendu dans l'interface utilisateur. Nous devons nous assurer qu'il ne casse rien et que la mise en page doit également être maintenue, sans conflit avec les actions / boutons, etc.
0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9
Ceci est un exemple de soumission. Une proposition pour sa suppression a été soumise, mais on dirait qu'elle va être rejetée.
0xbE2B28F870336B4eAA0aCc73cE02757fcC428dC9

Dernières sorties

Connecté dans / out

 avatar
Contributions
0
Deletions
0
Jetons
0
Propositions soumises
0
Propositions votées le
0
            
        
    

Note: c'est un squelette vraiment vraiment basique, juste pour démo d'intégration. S'il vous plaît ne comptez pas sur ce qui est le produit final!

Il est possible que vous manquiez le dossier dist dans le dossier web3 . Le logiciel est toujours en version bêta, donc des erreurs mineures sont toujours possibles. Pour contourner cela et installer web3 avec le dossier dist lancez npm install ethereum / web3.js --save .

Pour CSS, mettons quelque chose de rudimentaire dans public / assets / css / main.css :

 @supports (grid-area: auto) {
    .grid-container {
      affichage: grille;
      grid-template-columns: 6fr 5fr 4fr;
      grid-template-rows: 10rem;
      grille-colonne-écart: 0.5rem;
      grille-rangée-écart: 0.5rem;
      justifier-items: étirer;
      align-items: étirer;
      zones de gabarit de grille:
      "informations d'en-tête"
      "informations sur les événements de contenu";
      hauteur: 100vh;
    }
    .événements {
      zone de grille: événements;
    }
    .contenu {
      zone de grille: contenu;
    }
    .information {
      zone de grille: information;
    }
    .entête {
      grid-area: en-tête;
      text-align: centre;
    }

    .récipient {
        bordure: 1px noir uni;
        rembourrage: 15px;
        débordement-y: défile;
    }

    p {
        marge: 0;
    }
  }

corps {
    rembourrage: 0;
    marge: 0;
    font-family: sans-serif;
}

Puis, comme JS nous allons commencer avec ceci dans public / assets / js / app.js :

 var Web3 = require ('web3');

var web3 = nouveau Web3 (web3.currentProvider);
console.log (web3);

Que se passe-t-il ici?

Puisque nous avons convenu que nous supposerons que tous nos utilisateurs auront MetaMask installé, MetaMask injecte sa propre instance de Web3 dans le DOM de n'importe quelle page web visitée, Nous avons essentiellement accès au "fournisseur de portefeuille" de MetaMask directement sur notre site. En effet, si nous nous connectons à MetaMask alors que la page est ouverte, nous verrons cela dans la console:

 Le fournisseur MetaMask est actif

Remarquez comment le MetamaskInpageProvider est actif. En fait, si nous tapons web3.eth.accounts dans la console, tous les comptes auxquels nous avons accès via MetaMask seront imprimés:

 Le compte est imprimé dans la console

Ce compte particulier est, cependant, celui qui est ajouté à mon propre Metamask personnel par défaut et en tant que tel aura un solde de 0 eth. Cela ne fait pas partie de notre chaîne Ganache ou PoA:

 Un compte vide

Remarquez comment demander le solde de notre compte actif (MetaMasked) donne 0, tout en demandant l'équilibre de l'un de nos Les comptes blockchain privés donnent 100 ether (dans mon cas c'est Ganache, donc tous les comptes sont initialisés avec 100 ether).

A propos de la syntaxe

Vous remarquerez que la syntaxe de ces appels semble un peu étrange:

 web3.eth.getBalance ("0x35d4dCDdB728CeBF80F748be65bf84C776B0Fbaf", fonction (err, res) {console.log (JSON.stringify (res));});

Pour lire les données blockchain, la plupart des utilisateurs de MetaMask n'auront pas de noeud en cours d'exécution mais le demanderont à Infura ou à un autre noeud distant. Pour cette raison, nous pouvons pratiquement compter sur le décalage. Pour cette raison, les méthodes synchrones ne sont généralement pas supportées . Au lieu de cela, tout est fait par des promesses ou avec des rappels – tout comme avec l'étape de déploiement au début de ce post. Cela signifie-t-il que vous devez être intimement familier avec les promesses de développer JS pour Ethereum? Non. Cela signifie ce qui suit. Lorsque vous effectuez des appels JS dans le DOM …

  • fournissez toujours une fonction de rappel comme dernier argument à une fonction que vous appelez
  • supposer que ses valeurs de retour seront doubles: première erreur puis ]

Donc, fondamentalement, il suffit de penser à une réponse différée. Lorsque le nœud répond avec des données, la fonction que vous avez définie comme fonction de rappel sera appelée par JavaScript. Oui, cela signifie que vous ne pouvez pas attendre que votre code s'exécute ligne par ligne comme il est écrit!

Pour plus d'informations sur les promesses, les rappels et tout ce jazz asynchrone, voir ce post .

Informations sur le compte

Si nous ouvrons le squelette du site comme présenté ci-dessus, nous obtenons quelque chose comme ceci:

 Le site skeleton

real data

Session

Lorsque l'utilisateur n'est pas connecté à son extension MetaMask, la liste des comptes sera vide. Lorsque MetaMask n'est même pas installé, le fournisseur sera vide (indéfini). Quand ils sont connectés à MetaMask, le fournisseur sera disponible et offrira des informations de compte et d'interaction avec le nœud Ethereum connecté (live ou Ganache ou autre chose).

Astuce: pour tester, vous pouvez vous déconnecter de MetaMask par cliquez sur l'icône de votre avatar en haut à droite, puis sélectionnez Déconnexion. Si l'interface utilisateur ne ressemble pas à la capture d'écran ci-dessous, vous devrez peut-être activer l'interface utilisateur bêta en ouvrant le menu et en cliquant sur "Essayer la version bêta"

 MetaMask Se déconnecter

Tout d'abord, remplaçons tout le contenu de la colonne status par un message pour l'utilisateur s'il est déconnecté:

Vous semblez être déconnecté de MetaMask ou MetaMask n'est pas installé. Veuillez vous connecter à MetaMask - pour en savoir plus,             voir              tutoriel .

Le JS pour gérer cela ressemble à ceci (dans public / assets / js / main.js ):

 var loggedIn;

(fonction () {

    loggedIn = setLoggedIn (web3.currentProvider! == undefined && web3.eth.accounts.length> 0);

}) ();

function setLoggedIn (isLoggedIn) {
    let loggedInEl = document.querySelector ('div.logged.in');
    let loggedOutEl = document.querySelector ('div.logged.out');

    if (isLoggedIn) {
        loggedInEl.style.display = "bloquer";
        loggedOutEl.style.display = "aucun";
    } autre {
        loggedInEl.style.display = "none";
        loggedOutEl.style.display = "bloquer";
    }

    return isLoggedIn;
}

La ​​première partie – (function () { – enveloppe le peu de logique à exécuter une fois le site web chargé, donc tout ce qui s'y trouve sera exécuté immédiatement quand la page sera prête. setLoggedIn est appelé et une condition lui est passée La condition est la suivante:

  1. Le currentProvider de l'objet web3 est défini (c'est-à-dire qu'un client web3 est présent sur le site).
  2. Il existe un nombre différent de comptes disponibles, c'est-à-dire qu'un compte est disponible pour utilisation via ce fournisseur web 3. En d'autres termes, nous sommes connectés à au moins un compte.

Si ces conditions sont évaluées à true la fonction setLoggedIn rend le message "Logged out" invisible, et le message "Logged In" visible.

Tout ceci a l'avantage de pouvoir utiliser n'importe quel autre Si une alternative MetaMask apparaît finalement, elle sera inst compatible avec ce code car nous n'attendons pas explicitement MetaMask n'importe où.

Avatar du compte

Parce que chaque clé privée d'un portefeuille Ethereum est unique, elle peut être utilisée pour générer une image unique. C'est là que viennent les avatars colorés comme ceux que vous voyez dans le coin supérieur droit de MetaMask ou lorsque vous utilisez MyEtherWallet, bien que Mist, MyEtherWallet et MetaMask utilisent tous des approches différentes. Nous allons en générer un pour notre utilisateur connecté et l'afficher.

Les icônes de Mist sont générées avec la bibliothèque Blockies – mais personnalisées, car l'original a un générateur de nombres aléatoires cassé et peut produire images identiques pour différentes clés. Pour installer celui-ci, téléchargez ce fichier en un dans votre dossier assets / js . Puis, dans index.html nous l'avons inclus avant main.js :

             

Nous devrions aussi mettre à jour le conteneur logged.in :


Dans main.js nous lançons la fonction.

 if (isLoggedIn) {
      loggedInEl.style.display = "bloquer";
      loggedOutEl.style.display = "aucun";

      var icon = blockies.create ({// Toutes les options sont optionnelles
          seed: web3.eth.accounts [0]// seed utilisé pour générer des données d'icônes, défaut: random
          taille: 20, // largeur / hauteur de l'icône en blocs, par défaut: 8
          échelle: 8, // largeur / hauteur de chaque bloc en pixels, défaut: 4
      });

      document.querySelector ("div.avatar"). appendChild (icône);

Nous mettons à jour la section connectée du code JS pour générer l'icône et la coller dans la section avatar. Nous devrions aligner cela un peu avec CSS avant le rendu:

 div.avatar {
    largeur: 100%;
    text-align: centre;
    marge: 10px 0;
}

Maintenant, si nous actualisons la page une fois connecté à MetaMask, nous devrions voir notre icône d'avatar générée.

 L'icône Avatar

Soldes de compte

Maintenant nous allons afficher quelques informations sur le solde du compte.

Nous disposons d'un tas de fonctions en lecture seule que nous avons développées exclusivement à cette fin. Alors interrogeons la blockchain et demandons-lui des informations. Pour ce faire, nous devons appeler une fonction de contrat intelligent en suivant les étapes suivantes:

ABI

Obtenez l'ABI des contrats dont nous appelons les fonctions. L'ABI contient des signatures de fonction, donc notre code JS sait comment les appeler. En savoir plus sur ABI ici .

Vous pouvez obtenir l'ABI du jeton TNS et le StoryDAO en ouvrant le build / TNSToken.json et build / StoryDao.json fichiers dans votre dossier de projet après la compilation et en sélectionnant seulement la partie abi – donc la partie entre crochets [ et ] :

 Sélection ABI

Nous placerons cet ABI en haut de notre code JavaScript dans main.js comme ceci:

 Token et DAO ABI loaded

Notez que la capture d'écran ci-dessus montre l'insertion abrégée, réduite par mon éditeur de code (Microsoft Visual Code). Si vous regardez les numéros de ligne, vous remarquerez que l'ABI du jeton est de 400 lignes de code, et l'ABI du DAO est de 1000 autres, donc coller cela dans cet article n'aurait aucun sens.

2. Instanciez le jeton

 if (loggedIn) {

    var token = TNSToken.at ('0x3134bcded93e810e1025ee814e87eff252cff422');
    var story = StoryDao.at ('0x729400828808bc907f68d9ffdeb317c23d2034d5');
    token.balanceOf (web3.eth.accounts [0]fonction (erreur, résultat) {console.log (JSON.stringify (résultat))});
    story.getSubmissionCount (fonction (erreur, résultat) {console.log (JSON.stringify (résultat))});
// ...

Nous invoquons chaque contrat avec l'adresse qui nous est donnée par Truffle et créons une instance pour chacun – jeton et histoire respectivement. Ensuite, nous appelons simplement les fonctions (de manière asynchrone comme précédemment). La console nous donne deux zéros parce que le compte dans MetaMask a 0 jetons, et parce qu'il y a 0 soumissions dans l'histoire pour le moment.

 La ​​console produit deux zéros

3. Lecture et sortie de données

Enfin, nous pouvons remplir les données de profil de l'utilisateur avec les informations dont nous disposons.

Nous allons mettre à jour notre JavaScript:

 var loggedIn;

(fonction () {

    loggedIn = setLoggedIn (web3.currentProvider! == undefined && web3.eth.accounts.length> 0);

    if (loggedIn) {

        var token = TNSToken.at ('0x3134bcded93e810e1025ee814e87eff252cff422');
        var story = StoryDao.at ('0x729400828808bc907f68d9ffdeb317c23d2034d5');

        token.balanceOf (web3.eth.accounts [0]fonction (erreur, résultat) {console.log (JSON.stringify (résultat))});
        story.getSubmissionCount (fonction (erreur, résultat) {console.log (JSON.stringify (résultat))});

        readUserStats (). then (Utilisateur => renderUserInfo (Utilisateur));
    }

}) ();

fonction async readUserStats (adresse) {
    if (adresse === non défini) {
        adresse = web3.eth.accounts [0];
    }
    var Utilisateur = {
        numberOfSubmissions: await getSubmissionsCountForUser (adresse),
        numberOfDeletions: attendez getDeletionsCountForUser (adresse),
        isWhitelisted: await isWhitelisted (adresse),
        isBlacklisted: await isBlacklisted (adresse),
        numberOfProposals: await getProposalCountForUser (adresse),
        numberOfVotes: await getVotesCountForUser (adresse)
    }
    retourner l'utilisateur;
}

function renderUserInfo (Utilisateur) {
    console.log (Utilisateur);

    document.querySelector ('# user_submissions'). innerHTML = User.numberOfSubmissions;
    document.querySelector ('# user_deletions'). innerHTML = User.numberOfDeletions;
    document.querySelector ('# user_proposals'). innerHTML = User.numberOfProposals;
    document.querySelector ('# user_votes'). innerHTML = User.numberOfVotes;
    document.querySelector ('dd.user_blacklisted'). style.display = User.isBlacklisted? 'inline-block': 'aucun';
    document.querySelector ('dt.user_blacklisted'). style.display = User.isBlacklisted? 'inline-block': 'aucun';
    document.querySelector ('dt.user_whitelisted'). style.display = User.isWhitelisted? 'inline-block': 'aucun';
    document.querySelector ('dd.user_whitelisted'). style.display = User.isWhitelisted? 'inline-block': 'aucun';
}

async function getSubmissionsCountForUser (adresse) {
    if (adresse === non défini) {
        adresse = web3.eth.accounts [0];
    }
    return new Promise (fonction (résoudre, rejeter) {
        résoudre (0);
    });
}
async function getDeletionsCountForUser (adresse) {
    if (adresse === non défini) {
        adresse = web3.eth.accounts [0];
    }
    return new Promise (fonction (résoudre, rejeter) {
        résoudre (0);
    });
}
async function getProposalCountForUser (adresse) {
    if (adresse === non défini) {
        adresse = web3.eth.accounts [0];
    }
    return new Promise (fonction (résoudre, rejeter) {
        résoudre (0);
    });
}
async function getVotesCountForUser (adresse) {
    if (adresse === non défini) {
        adresse = web3.eth.accounts [0];
    }
    return new Promise (fonction (résoudre, rejeter) {
        résoudre (0);
    });
}
async function isWhitelisted (adresse) {
    if (adresse === non défini) {
        adresse = web3.eth.accounts [0];
    }
    return new Promise (fonction (résoudre, rejeter) {
        résoudre (faux);
    });
}
fonction async isBlacklisted (adresse) {
    if (adresse === non défini) {
        adresse = web3.eth.accounts [0];
    }
    return new Promise (fonction (résoudre, rejeter) {
        résoudre (faux);
    });
}

Et changés la section info de profil:


Vous remarquerez que nous avons utilisé des promesses lors de l'extraction des données, même si nos fonctions ne sont actuellement que des fonctions fictives: elles renvoient des données à plat immédiatement. En effet, chacune de ces fonctions aura besoin d'un temps différent pour récupérer les données que nous lui avons demandé de récupérer. Nous allons donc attendre leur fin avant de remplir l'objet User, puis de le transmettre dans le rendu. fonction qui met à jour les informations sur l'écran.

Si vous n'êtes pas familier avec les promesses de JS et que vous voulez en savoir plus, voir cet article

Pour l'instant, toutes nos fonctions sont fausses; nous devrons écrire avant de pouvoir lire quelque chose. Mais d'abord nous devrons être prêts à remarquer ces écritures!

Écouter les événements

Pour pouvoir suivre les événements émis par le contrat, nous devons les écouter – comme nous l'avons fait tous ceux émettent des déclarations dans le code pour rien. La partie centrale de l'interface simulée que nous avons construite est destinée à contenir ces événements.

Voici comment nous pouvons écouter les événements émis par la blockchain:

 // Events

var WhitelistedEvent = story.Whitelisted (fonction (erreur, résultat) {
    if (! error) {
        console.log (résultat);
    }
});

Ici nous appelons la fonction Whitelisted sur l'exemple story de notre contrat StoryDao, et nous passons un rappel. Ce rappel est automatiquement appelé à chaque fois que cet événement donné est déclenché. Ainsi, lorsqu'un utilisateur est ajouté à la liste blanche, le code enregistre automatiquement dans la console la sortie de cet événement.

Cependant, ceci n'obtiendra que le dernier événement du dernier bloc extrait par un réseau. Donc, s'il y a plusieurs événements Whitelisted tirés du bloc 1 à 10, cela ne nous montrera que ceux du bloc 10, le cas échéant. Une meilleure façon est d'utiliser cette approche:

 story.Whitelisted ({}, {fromBlock: 0, toBlock: 'latest'}). Get ((error, eventResult) => {
  if (erreur) {
    console.log ('Erreur dans le gestionnaire d'événements myEvent:' + erreur);
  } autre {
    // eventResult contient la liste des événements!
    console.log ('Evénement:' + JSON.stringify (eventResult [0] .args));
  }
});

Note: Mettez ci-dessus dans une section séparée au bas de votre fichier JS, un dédié aux événements.

Ici, nous utilisons la fonction get qui nous permet définir la plage de blocs à partir de laquelle récupérer les événements. Nous utilisons 0 au plus tard, ce qui signifie que nous pouvons récupérer tous les événements de ce type, jamais. Mais cela a le problème supplémentaire de conflit avec la méthode de surveillance ci-dessus. La méthode de surveillance sort l'événement du dernier bloc, et obtient la sortie de tous les événements. Nous avons besoin d'un moyen de faire en sorte que les JS ignorent les événements doubles. N'écrivez pas ceux que vous avez déjà récupérés dans l'histoire. Nous ferons cela plus bas, mais pour l'instant, traitons de la liste blanche

Liste blanche des comptes

Enfin, passons à quelques opérations d'écriture.

La première et la plus simple est la liste blanche. Rappelez-vous, pour obtenir la liste blanche, un compte doit envoyer au moins 0.01 ether à l'adresse du DAO. Vous obtiendrez cette adresse lors du déploiement. Si votre chaîne Ganache / PoA a redémarré entre les parties de ce cours, c'est bon, il suffit de relancer les migrations avec truffle migrate --reset et vous obtiendrez les nouvelles adresses pour le jeton et le DAO . Dans mon cas, l'adresse du DAO est 0x729400828808bc907f68d9ffdeb317c23d2034d5 et mon jeton est à 0x3134bcded93e810e1025ee814e87eff252cff422 .

Avec tout ce qui précède mis en place, essayons d'envoyer une quantité d'éther à l'adresse DAO . Essayons avec de l'éther 0,05 juste pour le fun, donc nous pouvons voir si le DAO nous donne aussi les jetons supplémentaires calculés pour le surpayer.

Note: n'oubliez pas de personnaliser la quantité de gaz – juste une autre zéro au-dessus de la limite 21000 – en utilisant l'icône marquée en rouge. Pourquoi est-ce nécessaire? Parce que la fonction qui est déclenchée par un simple envoi d'éther (la fonction de repli) exécute une logique supplémentaire qui dépasse 21000, ce qui est suffisant pour les envois simples. Nous devons donc dépasser la limite. Ne vous inquiétez pas: tout dépassement de cette limite est immédiatement remboursé. Pour une introduction sur le fonctionnement du gaz, voir ici .

 Envoi d'éther au DAO

 Personnalisation du montant de gaz

la transaction confirme (vous verrez cela dans MetaMask comme "confirmé"), nous pouvons vérifier le montant du jeton dans notre compte MetaMask. Nous devons d'abord ajouter notre jeton personnalisé à MetaMask afin qu'il puisse les suivre. Selon l'animation ci-dessous, le processus est le suivant: sélectionnez le menu MetaMask, faites défiler jusqu'à Add Tokens sélectionnez Custom Token collez l'adresse du jeton qui vous est donné par Truffe sur la migration, cliquez sur Suivant voir si la balance est bonne, puis sélectionnez Ajouter des jetons .

 Jeton ajoutant une animation

Pour 0.05 eth nous devrions avoir 400k jetons, et nous le faisons.

 Personnalisation de la quantité de gaz

Mais qu'en est-il de l'événement? Avons-nous été informés de cette liste blanche? Regardons dans la console

 Evénement dans la console

En effet, l'ensemble de données complet est là – l'adresse qui a émis l'événement, le numéro de bloc et le hash dans lequel cela a été extrait, etc. . Parmi ceux-ci se trouve l'objet args qui nous indique les données de l'événement: addr est l'adresse en liste blanche, et l'état indique s'il a été ajouté à la liste blanche ou retiré de celle-ci. Succès!

Si nous actualisons la page maintenant, l'événement est de nouveau dans la console. Mais comment? Nous n'avons pas ajouté de nouvelle liste blanche. Pourquoi l'événement s'est-il déclenché? La chose avec des événements dans EVM est qu'ils ne sont pas des choses uniques comme en JavaScript. Bien sûr, ils contiennent des données arbitraires et servent uniquement de sortie, mais leur sortie est enregistrée pour toujours dans la blockchain car la transaction qui les a provoqués est également enregistrée pour toujours dans la blockchain. Donc, les événements resteront après avoir été émis, ce qui nous évite d'avoir à les stocker quelque part et à les rappeler à la page d'actualisation!

Maintenant, ajoutons ceci à l'écran des événements dans l'interface utilisateur! Modifiez la section Events du fichier JavaScript comme suit:

 // Events

var plus hautBlock = 0;
var WhitelistedEvent = story.Whitelisted ({}, {fromBlock: 0, toBlock: "dernier"});

WhitelistedEvent.get ((error, eventResult) => {
  if (erreur) {
    console.log ('Erreur dans le gestionnaire d'événements en liste blanche:' + erreur);
  } autre {
    console.log (eventResult);
    let len ​​= eventResult.length;
    pour (let i = 0; i <len; i ++) {
      console.log (eventResult [i]);
      HighestBlock = plus hautBloc < eventResult[i].blockNumber ? eventResult[i].blockNumber : highestBlock;
      printEvent("Whitelisted", eventResult[i]);
    }
  }
});

WhitelistedEvent.watch(function(error, result) {
  if (!error && result.blockNumber > plus hautBloc) {
    printEvent ("Whitelisted", résultat);
  }
});

function printEvent (type, objet) {
  switch (type) {
    case "Whitelisted":
      laissez el;
      if (object.args.status === true) {
          el = "
  • Adresse de liste blanche" + object.args.addr + "
  • ";       } autre {           el = "
  • Adresse supprimée" + object.args.addr + "de la liste blanche!
  • ";       }       document.querySelector ("ul.eventlist"). innerHTML + = el;     Pause;     défaut:     Pause;   } }

    Wow, les événements se sont vite compliqués, hein? Ne vous inquiétez pas, nous allons clarifier.

    La variable highestBlock se souviendra du dernier bloc récupéré dans l'histoire. Nous créons une instance de notre événement et y attachons deux auditeurs. On est obtenir qui reçoit tous les événements de l'histoire et se souvient du dernier bloc. L'autre est montre qui surveille les événements «en direct» et se déclenche quand un nouveau apparaît dans le bloc le plus récent. L'observateur ne se déclenche que si le bloc qui vient d'arriver est plus grand que le bloc dont nous nous souvenons le plus haut, en s'assurant que seuls les nouveaux événements sont ajoutés à la liste des événements.

    Nous avons également ajouté un printEvent ] fonctionnent pour rendre les choses plus faciles; nous pouvons le réutiliser pour d'autres types d'événements aussi!

    Si nous testons cela maintenant, en effet, nous l'obtenons bien imprimé.

     L'événement est à l'écran

    Essayez de le faire vous même maintenant pour tous les autres événements que notre histoire peut émettre! Voyez si vous pouvez comprendre comment les gérer tous en même temps plutôt que d'avoir à écrire cette logique pour chacun d'entre eux. (Indice: définissez leurs noms dans un tableau, puis parcourez ces noms et enregistrez dynamiquement les événements!)

    Vérification manuelle

    Vous pouvez également vérifier manuellement la liste blanche et tous les autres paramètres publics du StoryDAO en l'ouvrant dans MyEtherWallet et appeler sa fonction de liste blanche

     Inspection de liste blanche

    Vous remarquerez que si nous vérifions le compte dont nous venons d'envoyer le montant de la liste blanche, nous aurons un back, indiquant que ce compte existe réellement dans la mise en correspondance de la liste blanche .

    Utilisez ce même menu de fonctions pour expérimenter avec d'autres fonctions avant de les ajouter à l'interface Web

    . une entrée

    Enfin, faisons un appel à la fonction d'écriture de notre interface utilisateur. Cette fois, nous soumettrons une entrée dans notre histoire. Nous devons d'abord effacer les entrées d'échantillons que nous avons mises au début. Modifier le HTML à ceci:

    Chapitre 0

    C'est une nuit pluvieuse dans le centre de Londres.


    ...

    Et quelques CSS de base:

     .submission_input textarea {
      largeur: 100%;
    }
    

    Nous avons ajouté une zone de texte très simple à travers laquelle les utilisateurs peuvent soumettre de nouvelles entrées.

    Commençons maintenant la partie JS.

    Commençons par accepter cet événement en en ajoutant un nouveau et en modifiant notre Fonction printEvent . Nous pouvons également refactoriser un peu la totalité de la section événement pour la rendre plus réutilisable.

     // Events
    
    var plus hautBlock = 0;
    var WhitelistedEvent = story.Whitelisted ({}, {fromBlock: 0, toBlock: "dernier"});
    var SubmissionCreatedEvent = story.SubmissionCreated ({}, {fromBlock: 0, toBlock: "dernier"});
    
    var events = [WhitelistedEvent, SubmissionCreatedEvent];
    pour (let i = 0; i < events.length; i++) {
      events[i].get(historyCallback);
      events[i].watch(watchCallback);
    }
    
    function watchCallback(error, result) {
      if (!error && result.blockNumber > upperBlock) {
        printEvent (result.event, result);
      }
    }
    
    function historyCallback (erreur, événementResult) {
      if (erreur) {
        console.log ('Erreur dans le gestionnaire d'événements:' + erreur);
      } autre {
        console.log (eventResult);
        let len ​​= eventResult.length;
        pour (let i = 0; i <len; i ++) {
          console.log (eventResult [i]);
          highestBlock = highestBlock <eventResult [i] .blockNumber? eventResult [i] .blockNumber: HighestBlock;
          printEvent (eventResult [i]. event, eventResult [i]);
        }
      }
    }
    
    function printEvent (type, objet) {
      laissez el;
      switch (type) {
        case "Whitelisted":
          if (object.args.status === true) {
              el = "
  • Adresse de liste blanche" + object.args.addr + "
  • ";       } autre {           el = "
  • Adresse supprimée" + object.args.addr + "de la liste blanche!
  • ";       }       document.querySelector ("ul.eventlist"). innerHTML + = el;     Pause;     case "SubmissionCreated":       el = "
  • Utilisateur" + object.args.submitter + "a créé un" + ((object.args.image)? "n image": "text") + "entrée: #" + objet.args.index + "de contenu" + object.args.content + "
  • ";       document.querySelector ("ul.eventlist"). innerHTML + = el;     Pause;     défaut:     Pause;   } }

    Maintenant tout ce que nous devons faire pour ajouter un nouvel événement est l'instancier, puis définir un cas pour cela.

    Ensuite, rendons possible la soumission.

     document.getElementById ("submission-body-btn"). addEventListener ("clic", fonction (e) {
        if (! loggedIn) {
            return false;
        }
    
        var text = document.getElementById ("submission-body-input"). value;
        text = web3.toHex (texte);
    
        story.createSubmission(text, false, {value: 0, gas: 400000}, function(error, result) {
            refreshSubmissions();
        });
    });
    
    function refreshSubmissions() {
        story.getAllSubmissionHashes(function(error, result){
            console.log(result);
        });
    }
    

    Here we add an event listener to our submission form which, once submitted, first rejects everything if the user isn’t logged in, then grabs the content and converts it to hex format (which is what we need to store values as bytes).

    Lastly, it creates a transaction by calling the createSubmission function and providing two params: the text of the entry, and the false flag (meaning, not an image). The third argument is the transaction settings: value means how much ether to send, and gas means how much of a gas limit you want to default to. This can be changed in the client (MetaMask) manually, but it’s a good starting point to make sure we don’t run into a limit. The final argument is the callback function which we’re already used to by now, and this callback will call a refresh function which loads all the submissions of the story. Currently, this refresh function only loads story hashes and puts them into the console so we can check that everything works.

    Note: ether amount is 0 because the first entry is free. Further entries will need ether added to them. We’ll leave that dynamic calculation up to you for homework. Tip: there’s a calculateSubmissionFee function in our DAO for this very purpose.

    At this point, we need to change something at the top of our JS where the function auto-executes on page load:

    if (loggedIn) {
    
        token.balanceOf(web3.eth.accounts[0]function(error, result) {console.log(JSON.stringify(result))});
        story.getSubmissionCount(function(error, result) {console.log(JSON.stringify(result))});
    
        web3.eth.defaultAccount = web3.eth.accounts[0]; // CHANGE
    
        readUserStats().then(User => renderUserInfo(User));
        refreshSubmissions(); // CHANGE
    } else {
        document.getElementById("submission-body-btn").disabled = "disabled";
    }
    

    The changes are marked with // CHANGE: the first one lets us set the default account from which to execute transactions. This will probably be made default in a future version of Web3. The second one refreshes the submissions on page load, so we get a fully loaded story when the site opens.

    If you try to submit an entry now, MetaMask should open as soon as you click Submit and ask you to confirm submission.

    Submitting an entry

    Confirming an entry

    You should also see the event printed out in the events section.

    Event confirmed

    The console should echo out the hash of this new entry.

    Hash printed

    Note: MetaMask currently has a problem with private network and nonces. It’s described here and will be fixed soon, but in case you get the nonce error in your JavaScript console when submitting entries, the stopgap solution for now is to re-install MetaMask (disabling and enabling will not work). REMEMBER TO BACK UP YOUR SEED PHRASE FIRST: you’ll need it to re-import your MetaMask accounts!

    Finally, let’s fetch these entries and display them. Let’s start with a bit of CSS:

    .content-submissions .submission-submitter {
      font-size: small;
    }
    

    Now let’s update the refreshSubmissions function:

    function refreshSubmissions() {
      story.getAllSubmissionHashes(function (error, result) {
        var entries = [];
        for (var i = 0; i < result.length; i++) {
          story.getSubmission(result[i], (err, res) => {
    
            if (res[2] === web3.eth.accounts[0]) {
              res[2] = 'you';
            }
            let el = "";
            el += '
    ';         el += '
    ' + web3.toAscii(res[0]) + '
    ';         el += '
    by: ' + res[2] + '
    ';         el += '
    ';         el += '
    ';         document.querySelector('.content-submissions').innerHTML += el;       });     }   }); }

    We roll through all the submissions, get their hashes, fetch each one, and output it on the screen. If the submitter is the same as the logged-in user, “you” is printed instead of the address.

    Rendered submission

    Let’s add another entry to test.

    Another entry

    Conclusion

    In this part, we developed the beginnings of a basic front end for our DApp.

    Since developing the full front-end application could just as well be a course of its own, we’ll leave further development up to you as homework. Just call the functions as demonstrated, tie them into a regular JavaScript flow (either via a framework like VueJS or plain old jQuery or raw JS like we did above) and bind it all together. It’s literally like talking to a standard server API. If you do get stuck, check out the project repo for the code!

    Other upgrades you can do:

    • detect when the web3 provider changes or when the number of available accounts changes, indicating log-in or log-out events and auto-reload the page
    • prevent the rendering of the submission form unless the user is logged in
    • prevent the rendering of the vote and delete buttons unless the user has at least 1 token, etc.
    • let people submit and render Markdown!
    • order events by time (block number), not by type!
    • make events prettier and more readable: instead of showing hex content, translate it to ASCII and truncate to 30 or so characters
    • use a proper JS framework like VueJS to get some reusability out of your project and to have better structured code.

    In the next and final part, we’ll focus on deploying our project to the live internet. Stay tuned!






    Source link

    Revenir vers le haut