Aller au contenu
Blockchain Association of Cameroon
BAC-L1-FOUND-1194 h

TypeScript : interface d'une application de chaîne

Construire l'interface qui connecte un portefeuille, construit une transaction et en suit l'état — sans jamais approcher la clé privée.

  • Connecter un portefeuille et demander une signature
  • Suivre le cycle de vie d'une transaction jusqu'à finalité
  • Rendre lisibles les états d'attente et d'échec

L'interface d'une application de chaîne est le seul endroit que l'utilisateur voit, et le seul où une erreur de présentation lui coûte de l'argent. Cet atelier vous apprend à écrire cette interface en TypeScript : représenter les montants sans perdre un chiffre, connecter un portefeuille et demander une signature sans jamais approcher la clé privée, suivre une transaction depuis le clic jusqu'à la finalité, et rendre lisibles les états d'attente et d'échec. Vous vous appuyez sur les requêtes RPC de l'atelier précédent. Le navigateur ne peut pas exécuter le compilateur TypeScript ni un vrai portefeuille : les exercices en ligne sont des lectures de code défectueux et de sorties de console ; l'interface réelle se construit dans le laboratoire autonome.

Leçons 1/4 · 15 min

TypeScript et montants : ne pas perdre un chiffre

Un formulaire de transfert qui ment

Un développeur écrit un formulaire de transfert. L'utilisateur tape un montant, le code le convertit en nombre, l'envoie. Tout marche en test avec des petits montants. Pourtant, en JavaScript, 0.1 + 0.2 donne 0.30000000000000004, et un grand montant peut être arrondi sans aucun message. Quand c'est de l'argent, une erreur silencieuse est la pire.

TypeScript est JavaScript auquel on ajoute des types : des étiquettes (nombre, texte, liste…) vérifiées par un compilateur avant l'exécution. tsc --noEmit lance cette vérification seule. Les types disparaissent à l'exécution : ils attrapent des bogues pendant que vous écrivez, mais ne protègent pas contre des données mal formées qui arrivent du réseau. Vite est l'outil qui sert la page en développement et la regroupe pour la production ; il transforme le TypeScript en JavaScript sans le vérifier, donc la vérification des types reste une étape à lancer vous-même.

Le piège des nombres

Un nombre JavaScript ordinaire est un nombre à virgule flottante sur 64 bits. Les entiers n'y sont exacts que jusqu'à 2^53 − 1 = 9 007 199 254 740 991 (Number.MAX_SAFE_INTEGER). Or beaucoup de chaînes comptent en très petites unités : un jeton à 18 décimales vaut 10^18 unités, bien au-delà.

Exemple. Un solde de 1 000 000 000 000 000 001 unités (un jeton plus une unité) :

console.log(Number(1000000000000000001n));   // 1000000000000000000
console.log(1000000000000000001n);           // 1000000000000000001n

Avec Number, l'unité en plus a disparu, sans erreur. Le type `bigint` (entiers de taille arbitraire, suffixe n) garde tout. Règles pratiques :

  • Représenter un montant en plus petite unité par un bigint, jamais par un number décimal.
  • Mélanger bigint et number dans une opération lève une erreur (TypeError), ce qui est un bon signal.
  • JSON.stringify échoue sur un bigint : convertissez en texte avec toString() pour l'envoyer ou le stocker.
  • Ne convertissez en texte décimal « lisible » qu'au dernier moment, à l'affichage.

Convertir ce que l'utilisateur tape

On évite le calcul à virgule : on découpe la chaîne.

function toUnits(text: string, decimals: number): bigint {
  if (!/^\d+(\.\d*)?$/.test(text)) throw new Error("Montant invalide");
  const [whole, frac = ""] = text.split(".");
  if (frac.length > decimals) throw new Error("Trop de décimales");
  return BigInt(whole + frac.padEnd(decimals, "0"));
}
// toUnits("1.5", 18) -> 1500000000000000000n
// toUnits("1.5.5", 18) -> erreur (validé en entier avant tout découpage)

On valide d'abord toute la chaîne avec une expression régulière : sans cela, un découpage qui ne garde que les deux premiers morceaux accepterait « 1.5.5 » et le lirait silencieusement comme 1.5. Le cas « trop de décimales » est refusé plutôt qu'arrondi : mieux vaut un message qu'un montant différent de celui que l'utilisateur croit envoyer. (Les littéraux bigint comme 1n demandent une cible de compilation ES2020 ou plus, ce qui est le cas des modèles Vite récents.)

Des types qui décrivent les états

Les unions de types rendent impossible de combiner n'importe quoi :

type Wallet =
  | { kind: "disconnected" }
  | { kind: "connected"; account: string; chainId: number };

Dans l'état disconnected, il n'y a pas de champ account : le compilateur refuse de le lire. C'est la base des deux leçons suivantes. Activez aussi le mode strict ("strict": true dans tsconfig.json) : sans lui, beaucoup de ces garanties s'évaporent.

Ce que cela ne garantit PAS

  • Les types sont une promesse faite au compilateur. Une réponse JSON reçue du nœud est de type inconnu tant que vous ne l'avez pas vérifiée : écrire as Balance fait taire le compilateur, pas le problème.
  • Un montant exact n'est pas un montant raisonnable : validez aussi le plafond, le solde disponible et l'adresse.
  • La validation du formulaire ne protège rien côté serveur ou chaîne : la chaîne refusera ou acceptera de toute façon.

À retenir : montants en bigint et en plus petite unité, texte découpé plutôt que calculé, unions de types pour les états.

Leçons 2/4 · 20 min

Connecter un portefeuille et demander une signature

Qui détient la clé ?

Le principe qui commande tout : votre page ne voit jamais la clé privée. Le portefeuille (une extension de navigateur, une application mobile ou un appareil matériel) la garde. La page lui adresse des demandes ; il montre à l'utilisateur ce qu'il va signer, et l'utilisateur accepte ou refuse. Aucune interface légitime ne demande de coller une clé privée ou une phrase de récupération : si la vôtre le faisait, ce serait une faille.

Cet atelier prend comme exemple la forme standard des portefeuilles de navigateur de type Ethereum, décrite par l'EIP-1193 : un objet « fournisseur » avec une méthode request. Les portefeuilles d'autres écosystèmes ont d'autres bibliothèques, mais le déroulement général est le même : détecter, demander l'accès, vérifier le réseau, demander une signature, gérer le refus. Dans l'écosystème Polkadot, par exemple, les extensions de portefeuille passent par une autre interface (activer l'extension, lire les comptes, obtenir un signataire), le réseau s'identifie en général par le hachage de son bloc de genèse plutôt que par un chain id numérique, et un refus se présente comme une erreur dont on lit le message, sans code 4001. Les détails dépendent de la bibliothèque et de sa version : lisez sa documentation.

Le déroulement, étape par étape

  1. Détecter si un portefeuille est présent. Sinon, état « pas de portefeuille » avec un message utile (quoi installer, quand).
  2. Demander l'accès aux comptes. L'utilisateur peut refuser : sur ce standard, l'erreur porte le code 4001. Ce n'est pas un bogue, c'est un choix. Attention : certaines bibliothèques encapsulent cette erreur (par exemple avec un code texte comme "ACTION_REJECTED" et le 4001 imbriqué dans un champ interne ; à vérifier dans la documentation et la version que vous utilisez). Testez donc plusieurs formes plutôt qu'un seul e.code === 4001.
  3. Lire le réseau (l'identifiant de chaîne, chain id) et le comparer à celui que votre application attend. Un autre réseau est un état à part entière : « mauvais réseau ».
  4. Demander une signature seulement quand l'utilisateur agit (un clic), jamais au chargement de la page.
  5. Écouter les changements : l'utilisateur peut changer de compte ou de réseau dans son portefeuille à tout moment.
type WalletState =
  | { kind: "no-wallet" }
  | { kind: "disconnected" }
  | { kind: "connecting" }
  | { kind: "rejected" }
  | { kind: "wrong-network"; account: string; chainId: number; expected: number }
  | { kind: "connected"; account: string; chainId: number }
  | { kind: "error"; message: string };

Sept états, chacun avec un affichage propre. Avec un seul booléen « connecté », la moitié des cas seraient confondus.

Signer un message pour se connecter

Pour prouver à un serveur qu'on contrôle un compte, on lui fait signer un message. Le serveur fournit un nonce à usage unique (même idée que dans l'atelier sur les comptes : empêcher le rejeu), une heure d'émission et une expiration :

app.example vous demande de vous connecter.
Compte : <adresse>
Nonce : 8f3a9c21   (fourni par le serveur, usage unique)
Émis : 2026-10-12T08:00:00Z
Expire : 2026-10-12T08:05:00Z

Le serveur vérifie la signature, le nonce (jamais utilisé), l'expiration et le domaine : la première ligne (ici app.example) rattache la signature à votre site, et un message établi pour un autre domaine doit être refusé, ce qui limite l'hameçonnage. Un standard existe pour cela chez les comptes de type Ethereum : l'EIP-4361 (« Sign-In with Ethereum »), dont le message précise aussi l'URI, la version et l'identifiant de chaîne. Signer un message de connexion ne dépense aucun frais et n'envoie aucune transaction.

Signature de message ou transaction : deux actes différents

Message à signerTransaction
Effet sur la chaîneaucunoui, si elle est incluse
Fraisnonoui
Usage typiqueprouver un contrôle de comptetransférer, appeler un contrat

Un piège connu : certains messages signés (autorisations hors chaîne, comme les « permit ») peuvent donner un droit sans transaction visible. Voir ci-dessous.

Hameçonnage de signature : ce que l'utilisateur voit contre ce qu'il signe

Le texte de votre bouton dit « Réclamer le cadeau », mais ce que le portefeuille demande de signer est une autorisation de dépenser les jetons de l'utilisateur, montant illimité, au profit d'une adresse inconnue. Si l'utilisateur signe sans lire, il a donné ses jetons. Les attaques réelles ont ce schéma.

Ce que votre code doit faire :

  • Construire le texte lisible à partir de l'objet qui sera réellement signé, jamais de deux sources séparées qui peuvent diverger.
  • Demander le strict minimum : montant exact plutôt qu'« illimité », durée courte.
  • Préférer les données structurées et typées (comme l'EIP-712 pour les portefeuilles qui le gèrent), que le portefeuille peut afficher champ par champ, aux octets bruts illisibles.
  • Protéger la page elle-même : une dépendance compromise ou une page altérée peut remplacer la demande. Verrouillez les dépendances (comme dans l'atelier Docker) et limitez les scripts chargés.

Ce que l'utilisateur doit faire : ne rien signer qu'il ne comprend pas, et vérifier l'adresse et le montant sur l'écran du portefeuille, pas sur la page.

Ce que cela ne garantit PAS

  • « Connecté » n'est pas « authentifié » : seule la vérification serveur d'une signature le prouve.
  • Une signature prouve le contrôle d'une clé à un instant, pas l'identité d'une personne.
  • Votre bon code ne protège pas un utilisateur qui signe à l'aveugle ; il réduit ce qu'on peut lui faire signer par surprise.

À retenir : la clé reste au portefeuille ; sept états ; nonce à usage unique ; texte construit depuis ce qui est signé.

Leçons 3/4 · 20 min

Suivre une transaction jusqu'à la finalité

Le clic n'est pas la fin

Dans une application ordinaire, un clic sur « Envoyer » appelle un serveur qui répond « fait ». Sur une chaîne, la réponse immédiate dit seulement que la transaction a été soumise. La suite se déroule en plusieurs étapes que l'interface doit montrer.

Les étapes d'une transaction

  1. En préparation : l'utilisateur remplit le formulaire.
  2. En attente de signature : le portefeuille a la main. L'utilisateur peut refuser.
  3. Soumise : le nœud a accepté la transaction et l'application connaît son hachage. Elle est dans le pool des transactions en attente (mempool). Rien n'est encore décidé.
  4. Incluse : un bloc la contient. Convention de cet atelier : une transaction dans le bloc de tête a 1 confirmation, et chaque bloc ajouté dessus en ajoute une.
  5. Finalisée : la chaîne la considère irréversible selon sa règle de finalité.

Les chemins d'échec :

  • Refusée par l'utilisateur (avant soumission).
  • Rejetée par le nœud à la soumission : nonce trop bas, frais trop bas, solde insuffisant, mauvais réseau (voir l'atelier sur les transactions).
  • Abandonnée ou remplacée : restée longtemps dans le pool avec des frais trop faibles, elle en est évincée, ou remplacée par une autre portant le même nonce.
  • Incluse mais échouée : l'exécution a échoué (par exemple un contrat a refusé l'opération). Sur les chaînes de type Ethereum, la transaction est bien dans un bloc, les frais sont en général payés, mais les changements demandés sont annulés. Le reçu (la trace que le nœud garde du résultat) porte un indicateur de succès ou d'échec : « incluse » ne veut donc pas dire « réussie ».
  • Retirée par une réorganisation : le bloc qui la contenait n'est plus dans la chaîne principale.

La finalité

Deux familles, à ne pas confondre :

  • Probabiliste : plus on empile de blocs dessus, moins une réorganisation est probable, sans jamais être impossible. Il n'y a pas de nombre magique : on choisit une profondeur selon ce qu'on risque.
  • Déterministe : un mécanisme de vote des validateurs déclare un bloc final ; au-delà, il ne peut plus changer sans que des validateurs trahissent leur engagement, ce qui est puni sur certains protocoles. Certains nœuds exposent une étiquette « finalisé » ; votre interface doit lire cette information, pas la deviner.

Exemple chiffré (chaîne fictive) : un bloc toutes les 6 secondes, et on affiche « finalisée » à 12 confirmations. Avec la convention de l'atelier, le bloc d'inclusion compte déjà pour 1 : il faut donc 11 blocs de plus, soit 11 × 6 = 66 secondes après l'inclusion (environ, car les durées de bloc varient). Sur une chaîne à 12 secondes par bloc, 11 × 12 = 132 secondes. L'interface doit montrer une progression (« 5 sur 12 ») plutôt qu'un cercle qui tourne.

Une machine à états typée

type TxState =
  | { kind: "awaiting-signature" }
  | { kind: "submitted"; hash: string }
  | { kind: "included"; hash: string; confirmations: number }
  | { kind: "finalized"; hash: string }
  | { kind: "failed"; hash?: string; reason: "user-rejected" | "node-rejected" | "reverted" }
  | { kind: "dropped"; hash: string }
  | { kind: "replaced"; hash: string; by: string }
  | { kind: "reorged"; hash: string };

function label(s: TxState): string {
  switch (s.kind) {
    case "awaiting-signature": return "Confirmez dans votre portefeuille";
    case "submitted": return "Envoyée, pas encore dans un bloc";
    case "included": return "Dans un bloc (" + s.confirmations + " confirmations)";
    case "finalized": return "Finalisée";
    case "failed": return "Échec";
    case "dropped": return "Abandonnée par le réseau";
    case "replaced": return "Remplacée par une autre transaction";
    case "reorged": return "Retirée par une réorganisation, en attente d'une nouvelle inclusion";
  }
}

Si vous ajoutez un nouvel état à l'union, le compilateur signale le switch qui ne le traite pas : en mode strict, avec un type de retour explicite (string ici), il refuse une fonction qui peut ne rien retourner. Pour une fonction qui ne renvoie rien, ou pour obtenir une erreur plus parlante, on ajoute une branche par défaut qui exige le type never (voir la leçon 4). C'est l'intérêt principal de typer les états.

Garder le fil

Dès que le hachage est connu, enregistrez-le dans le navigateur (par exemple localStorage, sans aucun secret, avec une clé de stockage qui contient l'adresse du compte et le chain id, pour qu'un autre compte ne reprenne pas la mauvaise transaction) : si l'utilisateur recharge la page ou perd sa connexion, l'application reprend le suivi. Pour suivre, on interroge le reçu du hachage ou on s'abonne aux nouveaux blocs (atelier précédent), avec reprise après coupure.

Ce que cela ne garantit PAS

  • Un hachage obtenu ne prouve pas que la transaction sera incluse.
  • Un nombre de confirmations n'est pas une garantie : c'est une probabilité qui décroît.
  • Votre interface rapporte ce que dit son nœud ; un nœud en retard ou mal informé peut se tromper.

À retenir : soumise, incluse, finalisée sont trois affirmations différentes ; « incluse » n'est pas « réussie ».

Leçons 4/4 · 15 min

Rendre lisibles l'attente et l'échec

Une interface honnête dit trois choses

À chaque état, l'utilisateur doit pouvoir lire : ce qui s'est passé, ce que l'on sait et ce que l'on ignore, et ce qu'il peut faire maintenant. Exemple pour une transaction restée longtemps en attente :

« Transaction envoyée il y a 3 minutes, pas encore dans un bloc. Elle peut encore être incluse : le réseau est peut-être chargé. Ne la renvoyez pas tout de suite. Numéro : 0x… [Copier] [Voir dans l'explorateur] »

À l'inverse, trois fautes classiques :

  • Afficher « Succès » dès que la soumission réussit. C'est faux : rien n'est inclus.
  • Un cercle qui tourne sans fin, sans heure, sans numéro, sans issue.
  • Un message du genre « Erreur : [object Object] » ou une trace technique brute.

Traduire les erreurs

Une erreur arrivée de la bibliothèque est de type inconnu en TypeScript (unknown dans un catch strict). On la reconnaît, on la classe, on la traduit :

type UserMessage = { title: string; hint: string; retry: boolean };

function describe(e: unknown): UserMessage {
  const code = typeof e === "object" && e !== null && "code" in e
    ? (e as { code: unknown }).code
    : undefined;
  if (code === 4001) {
    return { title: "Vous avez refusé la demande", hint: "Rien n'a été envoyé.", retry: true };
  }
  return { title: "Échec inattendu", hint: "Réessayez ; si cela persiste, notez l'heure.", retry: true };
}

Les erreurs que votre application doit savoir nommer, par ordre de fréquence en pratique : refus de l'utilisateur, mauvais réseau, solde insuffisant (pour le montant et les frais), frais trop faibles, nonce déjà utilisé, nœud injoignable, transaction échouée à l'exécution. Pour chacune, une phrase claire et une action. Gardez le détail technique accessible (« Afficher les détails ») pour les personnes qui assistent un utilisateur.

L'attente honnête

  • Immédiat : dès le clic, l'interface réagit (bouton désactivé, texte « En attente de votre portefeuille »). Sur une connexion lente, c'est la différence entre « ça a marché » et « je clique encore ».
  • Pas de double envoi : désactivez le bouton tant qu'une demande est en cours, et conservez la demande en cours dans l'état pour la reprendre.
  • Durée : affichez le temps écoulé, et la durée attendue quand on la connaît.
  • Dépassement : après un délai, changez le message sans conclure à tort : « Pas de confirmation après 3 minutes. La transaction peut encore être incluse. »
  • Pas seulement la couleur : un rouge ou un vert ne suffit pas (daltonisme, écran en plein soleil). Ajoutez un texte et une icône à sens clair.
  • Optimisme à l'étiquette : si vous mettez à jour le solde affiché avant confirmation, marquez-le « en attente ».

Hors ligne et lent

Beaucoup de vos utilisateurs sont sur une connexion mobile coupée de temps en temps. Prévoyez un état « hors ligne » visible, la reprise automatique quand le réseau revient, et une application légère : chargez le code de la page de transfert seulement quand on l'ouvre.

Un compilateur qui oblige à tout traiter

function assertNever(x: never): never {
  throw new Error("État non traité : " + JSON.stringify(x));
}
// dans un switch, la branche default appelle assertNever(state)

Quand quelqu'un ajoute l'état "replaced" et oublie l'affichage correspondant, la compilation échoue : l'oubli est attrapé avant les utilisateurs.

Ce que cela ne garantit PAS

  • Un beau message n'empêche pas l'échec ; il évite que l'utilisateur aggrave la situation (renvoyer deux fois, abandonner à tort).
  • Traduire une erreur sans la comprendre la déguise : testez chaque message avec la vraie cause (voir le laboratoire).
  • Un état « finalisé » affiché est aussi fiable que la source qui le déclare.

À retenir : dire ce qui s'est passé, ce qu'on sait, ce qu'on peut faire ; ne jamais annoncer un succès avant l'heure.

Laboratoire guidé

Le navigateur ne peut pas lancer le compilateur TypeScript ni un portefeuille : ces étapes sont des exercices de lecture et de diagnostic sur des extraits de code défectueux et des sorties de console. C'est une vraie compétence : avant d'écrire une interface, on doit reconnaître un montant arrondi en silence, un faux « succès », un message d'erreur trompeur. Les extraits sont courts et réalistes ; l'interface de transfert complète, avec suivi d'état, est le laboratoire autonome.

Le navigateur ne remplace pas un vrai terminal : ce laboratoire entraîne la lecture et le raisonnement ; la pratique réelle est dans le laboratoire autonome.

Un compte certifié est nécessaire

Les activités notées enregistrent des résultats à votre nom et servent de base à un titre. Elles exigent donc un dossier certifiant complet et un consentement en vigueur. Remplir le dossier ne vérifie pas votre identité ; le Secrétariat peut ensuite la vérifier (badge « Vérifié »).

  1. 1.Trouver le défaut sur un montant

    Une interface affiche le solde d'un utilisateur. Le nœud renvoie 1000000000000000001 unités (un jeton plus une unité, 18 décimales). Le code et la sortie :

    const raw = 1000000000000000001n;
    const shown = Number(raw) / 1e18;
    console.log(shown);        // 1
    console.log(Number(raw));  // 1000000000000000000

    Répondez à la question de la grille : quel est le défaut et quelle est la bonne approche ?

  2. 2.Trouver le faux succès

    Voici le gestionnaire du bouton « Envoyer » d'un collègue.

    async function onSend() {
      setStatus("signing");
      const hash = await wallet.sendTransaction(tx);
      setStatus("success");
      showToast("Paiement effectué !");
    }

    On suppose que sendTransaction retourne le hachage dès que le nœud a accepté la transaction. Répondez à la question de la grille : qu'est-ce qui est faux dans ce code, du point de vue de l'utilisateur ?

  3. 3.Compter les confirmations

    Journal de suivi d'une transaction (numéros fictifs). Convention de l'atelier : une transaction dans le bloc de tête a 1 confirmation.

    09:14:02  hachage 0x7c…  soumise
    09:14:20  reçu : incluse dans le bloc 200
    09:14:56  bloc de tête : 205

    Répondez à la question de la grille : combien de confirmations la transaction a-t-elle à 09:14:56 ?

  4. 4.Juger un message d'erreur

    Le gestionnaire d'erreurs d'une interface et ce qu'a fait l'utilisateur :

    try {
      await wallet.sendTransaction(tx);
    } catch (e) {
      showError("Erreur réseau, vérifiez votre connexion.");
    }

    L'utilisateur a simplement cliqué sur « Rejeter » dans la fenêtre du portefeuille. La bibliothèque a levé une erreur portant le code 4001. Répondez à la question de la grille : quel est le problème et quel comportement est correct ?

Exercice de sécurité — Signature piégée : ce que vous voyez, ce que vous signez

Une page de promotion affiche un gros bouton : « Réclamer 10 jetons gratuits — aucun frais ». L'utilisateur clique. Son portefeuille ouvre une fenêtre qui décode la demande ainsi (adresses fictives) :

Page          : promo.example
Action        : Autoriser la dépense de vos jetons (permit : signature hors chaîne)
Bénéficiaire  : 0xBBBB…0001  (adresse inconnue)
Montant       : illimité
Valable       : sans expiration
Frais         : aucun (aucune transaction n'est envoyée)

Le texte de la page ne mentionne ni autorisation, ni bénéficiaire, ni illimité. La page prétend que « signer est gratuit ». Une autorisation signée hors chaîne, du type « permit » (comme l'EIP-2612 pour les jetons ERC-20), ne coûte en effet aucun frais au moment de la signature, mais elle accorde un droit réel sur les jetons du compte. (Un approve classique, lui, est une transaction envoyée sur la chaîne, qui coûte des frais : ce n'est pas le cas décrit ici.)

Vous êtes l'auteur de l'interface d'une application légitime qui doit demander des autorisations, et vous relisez ce cas pour en tirer vos règles. Il faut distinguer ce qui protège l'utilisateur de ce qui ne le protège pas : ce n'est pas la page qui décide, c'est ce que le portefeuille affiche et ce que l'utilisateur lit.

Question de la grille : cochez TOUTES les affirmations exactes.

Questionnaire de l'atelier

Huit questions tirées d'une réserve plus large, seuil de réussite 70 %. Il s'ouvre quand tous les points de contrôle du laboratoire sont réussis. Vous pouvez le repasser : le tirage change.

Un compte certifié est nécessaire

Les activités notées enregistrent des résultats à votre nom et servent de base à un titre. Elles exigent donc un dossier certifiant complet et un consentement en vigueur. Remplir le dossier ne vérifie pas votre identité ; le Secrétariat peut ensuite la vérifier (badge « Vérifié »).

Laboratoire autonome (hors du site)

Non noté en ligne : à réaliser sur votre machine et à publier sur votre dépôt Git.

À faire sur votre machine, hors du site. Non noté en ligne ; l'interface est évaluée, entre autres, sur la clarté de ses états d'erreur.

  1. Créez un projet Vite en TypeScript avec le mode strict. Choisissez les bibliothèques (un client EVM comme ethers, ou une bibliothèque Polkadot) en lisant leur documentation, et notez leurs versions exactes dans le README : ce cours n'en impose pas.
  2. Utilisez uniquement un compte de test avec des fonds de test, créé pour l'occasion. Ne collez jamais de clé privée ni de phrase de récupération dans le code, un fichier ou une page.
  3. Implémentez les sept états du type WalletState de la leçon 2 (no-wallet, disconnected, connecting, rejected, wrong-network, connected, error) et réagissez aux deux événements qui les font changer : changement de compte, changement de réseau.
  4. Ajoutez un formulaire de transfert avec conversion exacte des montants (bigint, plus petite unité), refus des décimales en trop, et vérification du solde pour le montant ET les frais.
  5. Écrivez la machine à états de la transaction (en attente de signature, soumise, incluse avec compteur, finalisée, échouée, abandonnée, remplacée, retirée par une réorganisation), avec une branche exhaustive assertNever. Enregistrez le hachage dans localStorage et reprenez le suivi après rechargement.
  6. Provoquez chaque échec (refus dans le portefeuille, mauvais réseau, solde insuffisant) et vérifiez que le message nomme la cause et propose une action.
  7. Utilisez la limitation de débit des outils de développement du navigateur et coupez le réseau : vérifiez l'état hors ligne, la reprise et le retour visuel immédiat. Mesurez la taille de la page chargée et réduisez-la.

À retenir

  • Les montants se manipulent en bigint, dans la plus petite unité ; Number arrondit sans erreur au-delà de 2^53 − 1.
  • La clé privée reste dans le portefeuille ; la page envoie des demandes, gère le refus (code 4001 sur le standard EIP-1193) et le mauvais réseau comme des états normaux.
  • Ce que l'utilisateur lit doit être construit depuis ce qui sera réellement signé ; un message de connexion porte un nonce à usage unique et une expiration.
  • Soumise, incluse, finalisée sont trois affirmations différentes ; incluse n'est pas réussie, et un nombre de confirmations est une probabilité.
  • Des états typés et un switch exhaustif obligent à afficher chaque cas ; une bonne interface dit ce qui s'est passé, ce qu'on sait et ce qu'on peut faire.

Idées fausses courantes

  • Idée reçue : Number suffit pour les montants : s'il y avait un problème, JavaScript lèverait une erreur.

    En réalité : Au-delà de 9 007 199 254 740 991, Number arrondit en silence. Il faut un bigint, en plus petite unité.

  • Idée reçue : Une fois que le portefeuille a renvoyé un hachage, le paiement est fait.

    En réalité : Le hachage prouve la soumission. La transaction peut encore être abandonnée, échouer à l'exécution ou être retirée par une réorganisation.

  • Idée reçue : Signer un message est toujours sans risque, puisque cela ne coûte aucun frais.

    En réalité : Une signature hors chaîne peut accorder un droit réel sur des jetons. L'absence de frais ne dit rien de ce qui est autorisé.

Glossaire

bigint
Type JavaScript d'entier de taille arbitraire (suffixe n), qui ne perd aucun chiffre contrairement à Number au-delà de 2^53 − 1.
Portefeuille (wallet)
Logiciel ou appareil qui garde les clés privées, affiche ce qui est demandé et signe seulement après accord de l'utilisateur.
Identifiant de chaîne (chain id)
Nombre qui désigne un réseau précis ; il est inclus dans ce qui est signé pour qu'une transaction ne soit pas valide sur un autre réseau.
Reçu (receipt)
Trace que le nœud garde du résultat d'une transaction incluse, avec un indicateur de succès ou d'échec.
Finalité
Moment à partir duquel un bloc est considéré comme irréversible, de façon probabiliste (confirmations) ou déterministe (vote de validateurs).