Construire une dapp sur Tezos (édition 2023)

Feb 25 2023
Partie 3 sur 4-> Échange de jetons Maintenant, descendons dans le terrier du lapin et implémentons la fonctionnalité la plus complexe de la dapp : l'échange de XTZ et de tzBTC. Concevoir l'interface utilisateur Je dis « la plus complexe » car l'interface que vous êtes sur le point de créer comprend de nombreuses pièces mobiles et des calculs qui doivent être effectués au moment de la saisie et de la confirmation de l'utilisateur.

Partie 3 sur 4-> Échange de jetons

Maintenant, descendons dans le terrier du lapin et implémentons la fonctionnalité la plus complexe de la dapp : l'échange de XTZ et tzBTC.

Conception de l'interface utilisateur

Je dis "la plus complexe" car l'interface que vous êtes sur le point de construire comprend beaucoup de pièces mobiles et de calculs qui doivent être effectués au moment de la saisie et de la confirmation de l'utilisateur. Le contrat Liquidity Baking est également un peu pointilleux sur les données que vous devez envoyer pour échanger des jetons, vous devrez donc affiner notre code pour vous assurer qu'il se déroule comme sur des roulettes !

Voici une capture d'écran de l'interface utilisateur que vous visez :

Il y a 2 entrées de texte, celle de gauche est modifiable et permettra à l'utilisateur de saisir le montant de XTZ ou tzBTC qu'il souhaite échanger et celle de droite sera désactivée et affichera le montant correspondant qu'il obtiendra dans le autre jeton. Le bouton au milieu avec les 2 flèches permettra à l'utilisateur de basculer l'entrée entre XTZ et tzBTC.

Entrer dans les détails de la façon dont les entrées de texte sont implémentées dépasserait le cadre de ce didacticiel, mais vous pouvez le consulter dans le UserInput.sveltefichier.

Gestion des entrées utilisateur

En un mot, chaque entrée avec son icône de jeton et maxson champ est le même composant, le composant parent suit la position de chacun pour mettre à jour son interface utilisateur en conséquence. En interne, chaque composant d'entrée garde une trace de l'entrée de l'utilisateur et du solde disponible pour afficher des messages d'erreur si le solde est trop bas. Chaque mise à jour dans l'entrée est envoyée au composant parent pour ajuster l'interface utilisateur générale.

Chaque fois qu'une mise à jour est envoyée au composant parent ( SwapView.svelte), les données fournies avec la mise à jour sont transmises à la saveInputfonction :

import { 
	xtzToTokenTokenOutput, 
	tokenToXtzXtzOutput, 
	calcSlippageValue 
} from "../lbUtils";
  
const saveInput = ev => {
  const { token, val, insufficientBalance: insufBlnc } = ev.detail;
  insufficientBalance = insufBlnc;
  if (token === tokenFrom && val > 0) {
    inputFrom = val.toString();
    inputTo = "";
    if (tokenFrom === "XTZ") {
      // calculates tzBTC amount
      let tzbtcAmount = xtzToTokenTokenOutput({
        xtzIn: val * 10 ** XTZ.decimals,
        xtzPool: $store.dexInfo.xtzPool,
        tokenPool: $store.dexInfo.tokenPool
      });
      if (tzbtcAmount) {
        inputTo = tzbtcAmount.dividedBy(10 ** tzBTC.decimals).toPrecision(6);
      }
      // calculates minimum output
      minimumOutput = calcSlippageValue("tzBTC", +inputTo, +slippage);
    } else if (tokenFrom === "tzBTC") {
      // calculates XTZ amount
      let xtzAmount = tokenToXtzXtzOutput({
        tokenIn: val * 10 ** tzBTC.decimals,
        xtzPool: $store.dexInfo.xtzPool,
        tokenPool: $store.dexInfo.tokenPool
      });
      if (xtzAmount) {
        inputTo = xtzAmount.dividedBy(10 ** XTZ.decimals).toPrecision(8);
      }
      // calculates minimum output
      minimumOutput = calcSlippageValue("XTZ", +inputTo, +slippage);
    }
  } else {
    inputFrom = "";
    inputTo = "";
  }
};

  • les valeurs nécessaires aux calculs des montants des jetons sont déstructurées de l' ev.detailobjet
  • la fonction vérifie que les valeurs sont reçues du jeton actuellement actif (celui de gauche)
  • si ce jeton est XTZ, le montant en tzBTC est calculé via la xtzToTokenTokenOutputfonction (plus de détails ci-dessous)
  • si ce jeton est tzBTC, le montant en XTZ est calculé via la tokenToXtzXtzOutputfonction (plus de détails ci-dessous)
  • le montant minimum à prévoir en fonction du glissement fixé par l'utilisateur est calculé par la calcSlippagefonction

Échanger XTZ contre tzBTC et tzBTC contre XTZ

Maintenant, regardons les fonctions que nous avons introduites ci-dessus, xtzToTokenTokenOutputet tokenToXtzXtzOutput. Ils ont été adaptés du code de ce dépôt et vous permettent de calculer combien de tzBTC un utilisateur obtiendra en fonction du montant XTZ saisi et vice-versa.

export const xtzToTokenTokenOutput = (p: {
  xtzIn: BigNumber | number;
  xtzPool: BigNumber | number;
  tokenPool: BigNumber | number;
}): BigNumber | null => {
  let { xtzIn, xtzPool: _xtzPool, tokenPool } = p;
  let xtzPool = creditSubsidy(_xtzPool);
  let xtzIn_ = new BigNumber(0);
  let xtzPool_ = new BigNumber(0);
  let tokenPool_ = new BigNumber(0);
  try {
    xtzIn_ = new BigNumber(xtzIn);
    xtzPool_ = new BigNumber(xtzPool);
    tokenPool_ = new BigNumber(tokenPool);
  } catch (err) {
    return null;
  }
  if (
    xtzIn_.isGreaterThan(0) &&
    xtzPool_.isGreaterThan(0) &&
    tokenPool_.isGreaterThan(0)
  ) {
    const numerator = xtzIn_.times(tokenPool_).times(new BigNumber(998001));
    const denominator = xtzPool_
      .times(new BigNumber(1000000))
      .plus(xtzIn_.times(new BigNumber(998001)));
    return numerator.dividedBy(denominator);
  } else {
    return null;
  }
};

Il en va de même pour tokenToXtzXtzOutput:

export const tokenToXtzXtzOutput = (p: {
  tokenIn: BigNumber | number;
  xtzPool: BigNumber | number;
  tokenPool: BigNumber | number;
}): BigNumber | null => {
  const { tokenIn, xtzPool: _xtzPool, tokenPool } = p;
  let xtzPool = creditSubsidy(_xtzPool);
  let tokenIn_ = new BigNumber(0);
  let xtzPool_ = new BigNumber(0);
  let tokenPool_ = new BigNumber(0);
  try {
    tokenIn_ = new BigNumber(tokenIn);
    xtzPool_ = new BigNumber(xtzPool);
    tokenPool_ = new BigNumber(tokenPool);
  } catch (err) {
    return null;
  }
  if (
    tokenIn_.isGreaterThan(0) &&
    xtzPool_.isGreaterThan(0) &&
    tokenPool_.isGreaterThan(0)
  ) {
    let numerator = new BigNumber(tokenIn)
      .times(new BigNumber(xtzPool))
      .times(new BigNumber(998001));
    let denominator = new BigNumber(tokenPool)
      .times(new BigNumber(1000000))
      .plus(new BigNumber(tokenIn).times(new BigNumber(999000)));
    return numerator.dividedBy(denominator);
  } else {
    return null;
  }
};

Création d'une opération d'échange

L'échange des jetons est assez intensif car ce sont plusieurs pièces mobiles qui doivent jouer à l'unisson. Décrivons étape par étape ce qui se passe après que l'utilisateur a cliqué sur le bouton Swap :

const swap = async () => {
  try {
    if (isNaN(+inputFrom) || isNaN(+inputTo)) {
      return;
    }
  
  ...
  } catch (error) {
    console.log(error);
    swapStatus = TxStatus.Error;
    store.updateToast(true, "An error has occurred");
  }
};

Ensuite, vous mettez à jour l'interface utilisateur afin de montrer à l'utilisateur que la transaction se prépare :

enum TxStatus {
  NoTransaction,
  Loading,
  Success,
  Error
}

swapStatus = TxStatus.Loading;
store.updateToast(true, "Waiting to confirm the swap...");
const lbContract = await $store.Tezos.wallet.at(dexAddress);
const deadline = calcDeadline();

Après cela, vous créez le ContractAbstractiondepuis Taquito afin d'interagir avec le DEX et vous calculez également le délai.

Remarque : le contrat Liquidity Baking s'attend à ce que vous passiez un délai pour le swap, la transaction sera rejetée si le délai est expiré.

Échanger tzBTC contre XTZ

Maintenant, vous avez 2 situations : l'utilisateur a sélectionné XTZ ou tzBTC comme jeton à échanger. Commençons par tzBTC car la préparation de la transaction est plus compliquée :

if (tokenFrom === "tzBTC") {
  const tzBtcContract = await $store.Tezos.wallet.at(tzbtcAddress);
  const tokensSold = Math.floor(+inputFrom * 10 ** tzBTC.decimals);
  let batch = $store.Tezos.wallet
    .batch()
    .withContractCall(tzBtcContract.methods.approve(dexAddress, 0))
    .withContractCall(
    tzBtcContract.methods.approve(dexAddress, tokensSold)
    )
    .withContractCall(
    lbContract.methods.tokenToXtz(
        $store.userAddress,
        tokensSold,
        minimumOutput,
        deadline
      )
    )
    .withContractCall(tzBtcContract.methods.approve(dexAddress, 0));
  const batchOp = await batch.send();
  await batchOp.confirmation();
}

Remarque 1 : vous pouvez en savoir plus sur les comportements du contrat tzBTC et des autres contrats FA1.2 ici .

Note 2 : techniquement parlant, il n'est pas nécessaire de remettre la permission à zéro à la fin de la transaction (mais la remettre à zéro au début est nécessaire). C'est juste une pratique courante pour empêcher toute autorisation en attente inattendue.

Tout d'abord, vous créez le ContractAbstractioncontrat tzBTC lorsque vous êtes sur le point d'interagir avec lui. Une fois cela fait, vous calculez le nombre de jetons que vous devez approuver en fonction de nos calculs précédents.

Remarque : il ContractAbstractions'agit d'une instance très utile fournie par Taquito qui expose différents outils et propriétés pour obtenir des détails sur un contrat donné ou interagir avec celui-ci.

Après cela, vous utilisez l' API Batch fournie par Taquito. L'API Batch permet de regrouper plusieurs opérations en une seule transaction, afin d'économiser du gaz et du temps de traitement. Voilà comment cela fonctionne:

  1. Vous appelez la batch()méthode présente sur la propriété walletou contractde l'instance de laTezosToolkit
  2. Cela renvoie une instance de lot avec différentes méthodes que vous pouvez utiliser pour créer des transactions, dans notre exemple, withContractCall()est une méthode qui ajoutera un nouvel appel de contrat au lot d'opérations
  3. En tant que paramètre de withContractCall(), vous passez l'appel de contrat comme si vous l'appeliez tout seul, en utilisant le nom du point d'entrée sur la methodspropriété duContractAbstraction
  4. Dans ce cas, vous effectuez 1 opération par lots pour définir l'autorisation du LB DEX dans le contrat tzBTC sur zéro, 1 opération pour approuver le montant requis par le swap, 1 opération pour confirmer le swap dans le contrat LB DEX et 1 opération pour remettre l'autorisation du LB DEX à zéro
  5. Sur le lot renvoyé, vous appelez la .send()méthode pour falsifier la transaction, la signer et l'envoyer au mempool Tezos, qui renvoie une opération
  6. Vous pouvez awaitla confirmation de la transaction en faisant appel .confirmation()à l'opération renvoyée à l'étape ci-dessus.
  • L'adresse du compte qui recevra le XTZ
  • Le montant de tzBTC qui sera vendu pour le swap
  • Le montant prévu de XTZ qui sera reçu
  • Un délai après lequel la transaction expire

Passer de XTZ à tzBTC

Ce sera une entreprise beaucoup plus facile! Vérifions d'abord le code :

const op = await lbContract.methods
  .xtzToToken($store.userAddress, minimumOutput, deadline)
  .send({ amount: +inputFrom });
await op.confirmation();

  • L'adresse du compte qui recevra les jetons tzBTC
  • Le montant attendu de tzBTC à recevoir
  • La date limite

Vous souvenez-vous de la .send()méthode que vous appelez sur la sortie de l'appel du point d'entrée ? Si vous ne le saviez pas, vous pouvez passer des paramètres à cette méthode, l'un des plus importants est un montant de XTZ à envoyer avec la transaction. Passez simplement un objet avec une amountpropriété et une valeur de la quantité de tez que vous souhaitez attacher, et c'est tout !

Ensuite, comme toute autre transaction, vous obtenez un objet d'opération et .confirmation()l'appelez pour attendre que l'opération soit incluse dans un nouveau bloc.

Mise à jour de l'interface utilisateur

Que l'échange soit réussi ou non, il est crucial de fournir des commentaires à vos utilisateurs.

Si l'échange a réussi, vous récupérerez les nouveaux soldes de l'utilisateur et fournirez un retour visuel :

const res = await fetchBalances($store.Tezos, $store.userAddress);
if (res) {
  store.updateUserBalance("XTZ", res.xtzBalance);
  store.updateUserBalance("tzBTC", res.tzbtcBalance);
  store.updateUserBalance("SIRS", res.sirsBalance);
} else {
  store.updateUserBalance("XTZ", null);
  store.updateUserBalance("tzBTC", null);
  store.updateUserBalance("SIRS", null);
}
// visual feedback
store.updateToast(true, "Swap successful!");

Si l'échange échoue, vous serez redirigé vers la catchbranche où vous devrez également fournir un retour visuel et mettre à jour l'interface utilisateur :

swapStatus = TxStatus.Error;
store.updateToast(true, "An error has occurred");

Enfin (jeu de mots), vous vous déplacez vers la finallybranche pour réinitialiser l'interface utilisateur après 3 secondes :

finally {
  setTimeout(() => {
    swapStatus = TxStatus.NoTransaction;
    store.showToast(false);
  }, 3000);
}

Comme vous pouvez le constater d'après le code impliqué, l'échange de jetons est une action assez complexe et il y a quelques points que vous devez garder à l'esprit, concernant à la fois le code que vous écrivez et l'interface utilisateur que vous créez :

  • Essayez de structurer votre code en différentes étapes qui ne se mélangent pas, par exemple, étape 1 : mettre à jour l'UI avant de falsifier la transaction, étape 2 : falsifier la transaction, étape 3 : émettre la transaction, étape 4 : mettre à jour l'UI, etc. .
  • N'oubliez jamais de fournir un retour visuel à vos utilisateurs ! La préparation d'une nouvelle opération peut prendre jusqu'à 30 secondes lorsque le réseau n'est pas encombré, et même plus s'il y a beaucoup de trafic. Les utilisateurs se demanderont ce qui se passe si vous ne les faites pas attendre. Un spinner ou une animation de chargement est généralement une bonne idée pour indiquer que l'application attend une sorte de confirmation.
  • Désactivez l'interface utilisateur pendant que la transaction est dans le mempool ! Vous ne voulez pas que les utilisateurs cliquent sur le bouton Swap une deuxième fois (ou une troisième ou une quatrième !) pendant que la blockchain traite la transaction qu'ils ont déjà créée. En plus de leur coûter plus cher, cela peut également les confondre et créer des comportements inattendus dans votre interface utilisateur.
  • Réinitialisez l'interface utilisateur à la fin. Personne ne veut cliquer sur le bouton Actualiser après une interaction avec la blockchain car l'interface utilisateur semble bloquée dans son état précédent. Assurez-vous que l'interface est dans le même état (ou similaire) que lorsque l'utilisateur l'a ouverte pour la première fois.