Construire une dapp sur Tezos (édition 2023)

Feb 25 2023
Partie 1 sur 4-> Configurer la dapp Comme la dapp sera construite avec TypeScript, une bonne connaissance de ce langage de programmation est requise. Nous utiliserons le framework Svelte pour développer l'application, aucune connaissance préalable n'est requise car il est assez intuitif à utiliser et j'expliquerai comment cela fonctionne en cours de route.

Partie 1 sur 4-> Configurer la dapp

Comme la dapp sera construite avec TypeScript , une bonne connaissance de ce langage de programmation est requise. Nous utiliserons le framework Svelte pour développer l'application, aucune connaissance préalable n'est requise car il est assez intuitif à utiliser et j'expliquerai comment cela fonctionne en cours de route.

Comme 99% des dapps de l'écosystème, cette dapp utilisera Taquito , une bibliothèque TypeScript qui offrira une bien meilleure expérience développeur pour utiliser la blockchain Tezos.

Le contrat de Liquidity Baking

Il existe un contrat spécial sur Tezos appelé le contrat Liquidity Baking . Ce contrat est un échange décentralisé (ou DEX) qui ne gère que 3 jetons : XTZ (le jeton natif de Tezos), tzBTC (un jeton enveloppé pour utiliser Bitcoin sur Tezos) et SIRS (pour Sirius, le jeton qui représente une quantité égale de liquidité en XTZ et tzBTC ajoutée au contrat).

La particularité de ce contrat est qu'à chaque fois qu'un nouveau bloc est cuit sur Tezos, 2,5 XTZ sont ajoutés au contrat. Les utilisateurs devraient apporter tzBTC afin de maintenir l'équilibre de la liquidité DEX et la stabilité du prix du SIRS.

Le contrat est également entièrement public, ce qui signifie que toute personne disposant d'un portefeuille Tezos peut interagir avec lui pour échanger XTZ contre tzBTC et vice-versa, fournir de la liquidité ou la supprimer, ce que nous allons faire dans ce tutoriel.

Qu'allons-nous construire ?

Dans ce didacticiel, nous allons créer une interface dapp qui interagit avec le contrat LB pour échanger des jetons, ajouter de la liquidité et la supprimer. La dapp gérera différentes actions :

  • Afficher les informations des utilisateurs comme leur solde XTZ, tzBTC et SIRS et les mettre à jour après chaque transaction
  • Connecter et déconnecter le portefeuille des utilisateurs
  • Afficher les informations du portefeuille comme son état de connexion et le réseau auquel il est connecté
  • Affichage de différentes interfaces pour échanger des jetons, ajouter et supprimer des liquidités
  • Autoriser les utilisateurs à échanger XTZ contre tzBTC et tzBTC contre XTZ
  • Permettre aux utilisateurs d'ajouter de la liquidité en fournissant XTZ et tzBTC et en obtenant SIRS en échange
  • Permettre aux utilisateurs de retirer de la liquidité, c'est-à-dire d'échanger des jetons SIRS et d'obtenir des jetons XTZ et tzBTC en échange.

Comme l'application décentralisée est finalement une application Web, nous utiliserons les outils suivants pour la construire :

  • Svelte pour le framework JavaScript
  • TypeScript pour rendre notre code JavaScript plus sûr et plus expressif
  • Sass comme préprocesseur CSS
  • Vite pour regrouper l'application (prononcé comme veet )
  • Taquito va interagir avec la blockchain Tezos
  • Beacon et la bibliothèque wrapper fournie par Taquito pour utiliser un portefeuille Tezos
  • Svelte =>https://svelte.dev/
  • Tapuscrit =>https://www.typescriptlang.org/
  • Sas =>https://sass-lang.com/
  • Vite =>https://vitejs.dev/
  • Taquito =>https://tezostaquito.io/
  • balise =>https://docs.walletbeacon.io/
  • Repo GitHub avec le dapp =>https://github.com/claudebarde/tezos-dev-portal-tutorial

Comme nous construisons une application Web avec le framework Svelte, les étapes de configuration du projet seront très similaires à celles que vous suivrez pour configurer toute autre application Web.

Dans ce tutoriel, nous allons fabriquer un Svelte SPA, nous n'aurons donc pas besoin de SvelteKit, ce qui nous facilitera également la vie.

La première chose à faire est d'installer Svelte avec TypeScript et Vite :

npm create vite@latest lb-dex -- --template svelte-ts
cd lb-dex
npm install

npm install --save-dev sass
npm install @taquito/taquito @taquito/beacon-wallet

Il y a quelques autres bibliothèques que nous devons installer :

npm install --save-dev buffer events vite-compatible-readable-stream

Une fois que tout est installé, il faut mettre en place la bonne configuration.

Dans votre appdossier, vous verrez le vite.config.jsfichier, c'est le fichier qui contient la configuration dont Vite a besoin pour exécuter et regrouper votre application. Effectuez les modifications suivantes :

import { defineConfig, mergeConfig } from "vite";
import path from "path";
import { svelte } from "@sveltejs/vite-plugin-svelte";

export default ({ command }) => {
  const isBuild = command === "build";
  
  return defineConfig({
    plugins: [svelte()],
    define: {
      global: {}
    },
    build: {
      target: "esnext",
      commonjsOptions: {
        transformMixedEsModules: true
      }
    },
    server: {
      port: 4000
    },
    resolve: {
      alias: {
        "@airgap/beacon-sdk": path.resolve(
          path.resolve(),
          `./node_modules/@airgap/beacon-sdk/dist/${
            isBuild ? "esm" : "cjs"
          }/index.js`
        ),
        // polyfills
        "readable-stream": "vite-compatible-readable-stream",
        stream: "vite-compatible-readable-stream"
      }
    }
  });
};

  • On se met globalà {}et on fournira plus tard l' globalobjet dans notre fichier HTML
  • Nous fournissons un chemin vers le SDK Beacon
  • Nous fournissons des polyfills pour readable-streametstream

Voici ce que vous devriez avoir :

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script>
      const global = globalThis;
    </script>
    <script type="module">
      import { Buffer } from "buffer";
      window.Buffer = Buffer;
    </script>
    <title>Liquidity Baking DEX</title>
  </head>
  <body>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Remarque : cette configuration est requise pour exécuter le SDK Beacon avec une application Vite. Taquito fonctionne complètement hors de la boîte et ne nécessite aucun réglage.

Une fois que nous avons mis à jour la configuration dans le vite.config.jsfichier et dans le index.htmlfichier, notre projet est configuré avec succès ! Vous pouvez exécuter npm run devdans votre terminal à la racine du projet pour vérifier que tout fonctionne correctement, le dapp doit fonctionner surhttp://localhost:4000

Maintenant, commençons à écrire du code et à configurer la dapp !

Configurer la dapp

Si vous avez réussi jusqu'ici et que votre application fonctionne sur http://localhost:4000, félicitations !

Maintenant, nous devons configurer le dapp afin d'utiliser Taquito et Beacon.

Structure du fichier

Le point d'entrée de chaque application Svelte est un fichier appelé App.svelte, c'est là que vous importerez tous vos composants à regrouper dans votre application finale. La structure de fichier de notre projet ressemble à ceci :

- src
	- assets
		- svelte.png
	- lib
		- AddLiquidityView.svelte
		- Interface.svelte
		- RemoveLiquidity.svelte
		- Sidebar.svelte
		- SirsStats.svelte
		- SwapView.svelte
		- Toast.svelte
		- UserInput.svelte
		- UserStats.svelte
		- Wallet.svelte
	- styles
		- index.scss
		- settings.scss
	- App.svelte
	- config.ts
	- lbUtils.ts
	- main.ts
	- store.ts
	- types.ts
	- utils.ts
- index.html
- svelte.config.js
- tsconfig.json
- vite.config.js

  • assets -> contient le favicon (ici, c'est le favicon Svelte par défaut, mais vous pouvez en choisir un autre)
  • lib -> contient les différents composants qui vont composer notre interface, voici ce que chacun fait :
    - SwapView.svelte: l'interface pour échanger les jetons XTZ et tzBTC
    - AddLiquidityView.svelte: l'interface pour ajouter de la liquidité au LB DEX
    - RemoveLiquidity.svelte: l'interface pour retirer de la liquidité du LB DEX
    - Interface.svelte: le composant d'ordre supérieur pour contenir les différentes vues pour interagir avec le LB DEX
    - Sidebar.svelte: le composant pour naviguer entre les différentes interfaces et connecter ou déconnecter le portefeuille
    - SirsStats.svelte: le composant pour afficher le montant de XTZ, tzBTC, et SIRS présents dans le contrat
    - Toast.svelte: un composant simple pour afficher la progression des transactions et autres messages lors de l'interaction avec le contrat
    - UserInput.svelte: un composant utilitaire pour faciliter l'interaction et le contrôle des champs de saisie
    - UserStats.svelte: le composant pour afficher le solde de l'utilisateur en XTZ, tzBTC et SIRS
    - Wallet.svelte: le composant pour gérer les interactions du portefeuille
  • styles -> contient les fichiers SASS pour styliser différents éléments de notre interface
  • App.svelte -> le point d'entrée de l'application
  • config.ts -> différentes valeurs immuables nécessaires à l'application et enregistrées dans un fichier séparé pour plus de commodité
  • lbUtils.ts -> différentes méthodes pour calculer les valeurs nécessaires pour interagir avec le contrat Liquidity Baking
  • main.ts -> c'est là que le JavaScript de l'application est regroupé avant d'être injecté dans le fichier HTML
  • store.ts -> un fichier avec un magasin Svelte pour gérer l'état dapp
  • types.ts -> types TypeScript personnalisés
  • utils.ts -> différentes méthodes utilitaires

import App from './App.svelte'
import "./styles/index.scss";

const app = new App({
 target: document.body
});
export default app;

Remarque : j'aime aussi cibler la bodybalise pour injecter le HTML produit par JavaScript au lieu d'un à divl'intérieur du body, mais c'est un choix personnel et vous êtes libre d'utiliser un divà la place

Avant de continuer, voici à quoi ressemble un fichier Svelte :

<script lang="ts">
  ... your TypeScript code
</script>

<style lang="scss">
   ... your SASS code
</style>

... your HTML code

Il existe une scriptbalise avec un langattribut défini sur tspour TypeScript, une stylebalise avec un langattribut défini sur scsspour SASS et le reste du code dans le fichier sera interprété comme HTML.

Configuration de la dapp

Maintenant, configurons différentes choses dans notre App.sveltefichier.

La partie HTML va simplement rassembler tous les composants d'ordre supérieur :

<main>
  <Toast />
  {#if $store.Tezos && $store.dexInfo}
    <Sidebar />
    <Interface />
  {:else}
    <div>Loading</div>
  {/if}
</main>

La partie SASS importera différents paramètres et appliquera un style à la mainbalise :

@import "./styles/settings.scss";

main {
 display: grid;
 grid-template-columns: 250px 1fr;
 gap: $padding;
 padding: $padding;
 height: calc(100% - (#{$padding} * 2));
}
@media screen and (max-height: 700px) {
 main {
   padding: 0px;
   height: 100%;
 }
}

import { onMount } from "svelte";
import { TezosToolkit } from "@taquito/taquito";
import store from "./store";
import { rpcUrl, dexAddress } from "./config";
import Sidebar from "./lib/Sidebar.svelte";
import Interface from "./lib/Interface.svelte";
import Toast from "./lib/Toast.svelte";
import type { Storage } from "./types";
import { fetchExchangeRates } from "./utils";

  • TezosToolkitest la classe qui vous donne accès à toutes les fonctionnalités de Taquito
  • storeest une fonctionnalité Svelte pour gérer l'état de la dapp
  • Depuis le config.tsfichier, nous importons rpcUrl(l'URL du nœud RPC Tezos) et dexAddress, l'adresse du contrat Liquidity Baking
  • Storageest un type personnalisé qui représente le type de signature du stockage LB DEX
  • fetchExchangeRatesest une fonction pour récupérer les taux de change de XTZ et tzBTC (plus de détails ci-dessous)

onMount(async () => {
  const Tezos = new TezosToolkit(rpcUrl);
  store.updateTezos(Tezos);
  const contract = await Tezos.wallet.at(dexAddress);
  const storage: Storage | undefined = await contract.storage();

  if (storage) {
    store.updateDexInfo({ ...storage });
  }
  // fetches XTZ and tzBTC prices
  const res = await fetchExchangeRates();
  if (res) {
    store.updateExchangeRates([
      { token: "XTZ", exchangeRate: res.xtzPrice },
      { token: "tzBTC", exchangeRate: res.tzbtcPrice }
    ]);
  } else {
    store.updateExchangeRates([
      { token: "XTZ", exchangeRate: null },
      { token: "tzBTC", exchangeRate: null }
    ]);
  }
});

Après cela, nous voulons récupérer le stockage du LB DEX pour obtenir les montants de XTZ, tzBTC et SIRS dans le contrat. Nous créons un ContractAbstraction, une instance fournie par Taquito avec différentes propriétés et méthodes utiles pour travailler avec les contrats intelligents Tezos. À partir de ContractAbstraction, nous pouvons appeler la storageméthode qui renvoie un objet JavaScript qui représente le stockage du contrat donné. On passe ensuite le stockage à la updateDexInfométhode présente sur le storepour mettre à jour ces données et les afficher à l'utilisateur.

Pour finir, nous devons récupérer les taux de change pour XTZ et tzBTC pour effectuer les conversions requises par ce type d'application. Le utils.tsfichier contient une fonction qui va nous aider ici :

export const fetchExchangeRates = async (): Promise<{
  tzbtcPrice: number;
  xtzPrice: number;
} | null> => {
  const query = `
      query {
        overview { xtzUsdQuote },
        token(id: "KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn") { price }
      }
    `;
  const res = await fetch(`https://analytics-api.quipuswap.com/graphql`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      query
    })
  });
  if (res.status === 200) {
    const resData = await res.json();
    let xtzPrice = resData?.data?.overview?.xtzUsdQuote;
    let tzbtcPrice = resData?.data?.token?.price;
    // validates the 2 values
    if (xtzPrice && tzbtcPrice) {
      xtzPrice = +xtzPrice;
      tzbtcPrice = +tzbtcPrice;
      if (!isNaN(xtzPrice) && !isNaN(tzbtcPrice)) {
        // tzBTC price is given in XTZ by the API
        tzbtcPrice = tzbtcPrice * xtzPrice;
        return { tzbtcPrice, xtzPrice };
      }
    } else {
      return null;
    }
  } else {
    return null;
  }
};

Dans la seconde partie, wallet et tokens de l'utilisateur =>