Aller au contenu

FoxTagger Snap : association d'adresses avec des tags définis par l'utilisateur

Lauréat d'un Hackathon InterIIT

12 min de lecture
FoxTagger Snap : association d'adresses avec des tags définis par l'utilisateur

MetaMask Snaps est la feuille de route pour faire de MetaMask le portefeuille le plus extensible au monde. En tant que développeur, vous pouvez intégrer vos fonctionnalités et vos API à MetaMask de manières totalement nouvelles. Les développeurs Web3 sont au cœur de cette croissance, et cette série vise à mettre en lumière les MetaMask Snaps innovants qui sont développés aujourd'hui.

FoxTagger Snap

Dépôt du Snap : https://github.com/shree675/FoxTagger

FoxTagger est un outil qui facilite l'association d'adresses avec des étiquettes définies par l'utilisateur, afin d'aider les utilisateurs à surveiller leurs dépenses en les alertant et en affichant des analyses d'utilisation.

Présentez-nous la mise en œuvre technique ?

La configuration technique

Notre extension MetaMask, FoxTagger, se compose de deux composants principaux : un backend MetaMask Snaps (alias le snap) et un frontend Gatsby.js. Le frontend est une application web simple qui héberge l'interface utilisateur, mais il fait également office de DApp compagnon pour notre application Snaps.

La majeure partie des fonctionnalités est assurée par le snap, mais le reste est géré par le frontend pour permettre aux utilisateurs de demander des montants à d'autres personnes et/ou de configurer leurs étiquettes via notre interface. La fonctionnalité demande de montant utilise le protocole XMTP, tandis que la fonctionnalité d'étiquetage utilise l'API MetaMask Snaps.

Nous avons utilisé le snap monorepo comme modèle de départ. L'implémentation complète de notre projet est disponible ici.

L'application Snaps

Dans l'ensemble de notre implémentation, nous utilisons les fonctionnalités principales suivantes fournies par l'API Snaps :

  1. Stockage persistant

  2. Notifications

  3. Tâches cron

  4. Aperçus de transactions

Notre implémentation commence par exposer certaines fonctions au frontend dans packages\snap\src\index.ts.

export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => {
  switch (request.method) {
    case 'getPersistentStorage':
      return await getPersistentStorage();
    case 'setPersistentStorage':
      await setPersistentStorage(
        request.params as void | Record<string, unknown>,
      );
      return null;
    case 'clearPersistentStorage':
      await clearPersistentStorage();
      return null;

    default:
      throw new Error('Method not found.');
  }
};
Copier

Les fonctions getPersistenceStorage et setPersistenceStorage sont fondamentales, car elles facilitent le stockage et la récupération des étiquettes et d'autres informations. Elles sont définies comme indiqué dans l'extrait de code ci-dessous.

export const getPersistentStorage = async () => {
  return await wallet.request({
    method: 'snap_manageState',
    params: ['get'],
  });
};

export const clearPersistentStorage = async () => {
  await wallet.request({
    method: 'snap_manageState',
    params: ['clear'],
  });
};

export const setPersistentStorage = async (
  data: Record<string, unknown> | void,
) => {
  await wallet.request({
    method: 'snap_manageState',
    params: ['update', data],
  });
};
Copier

Pour toutes les implémentations de fonctionnalités, nous avons créé une structure de données adaptée à notre stockage persistant.

{
    "from_account0": {
      mainMapping: {
        "to_account0": ["tag0","tag1"],
        ...
      },
      usage: {
        "tag0": {
          limit: "100000000000",
          used: "800000",
          notified: false
        },
        ...
      },
      latestHash: "transaction_hash0"
    },
    ...
  }
Copier

Un utilisateur peut avoir plusieurs comptes, et chaque compte possède ses propres étiquettes, son historique d'utilisation et ses limites définies par l'utilisateur.

Aperçus de transactions

La fonctionnalité d'aperçus de transactions affiche les étiquettes associées, leurs dépenses correspondantes et les alertes éventuelles lors d'une transaction en cours.

Dans le même fichier index.ts, nous ajoutons un gestionnaire d'aperçus de transactions qui nous permet d'intercepter une transaction en cours et d'interagir avec le portefeuille MetaMask.

export const onTransaction: OnTransactionHandler = async ({ transaction }) => {
  const insights = await getDetails(transaction);

  return {
    insights,
  };
};
Copier

La fonction getDetails implémente toute la logique de calcul des dépenses et d'affichage des alertes. Elle est implémentée dans packages\snap\src\transaction.ts.

export const getDetails = async (transaction: Record<string, unknown>) => {
  const toAddress = (transaction.to as string).toLowerCase();
  const account = (transaction.from as string).toLowerCase();
  const completeStorage = (await getPersistentStorage()) as any;

  if (!completeStorage?.[account]) {
    throw new Error('Storage initialization failed.');
  }

  const storage = completeStorage[account];
  if (!storage.mainMapping || !storage.usage) {
    throw new Error('Data corrput. Please re-install the snap.');
  }

  const tagList = storage.mainMapping[toAddress];
  if (!tagList?.length) {
    return {
      Tag: NO_TAG_MESSAGE,
    };
  }

  let tags = '';
  let alerts = '';
  let usageMsg = '';

  for (const tag of tagList) {
    if (tags === '') {
      tags += `${tag}`;
    } else {
      tags += `, ${tag}`;
    }
    let { used } = storage.usage[tag];
    let { limit } = storage.usage[tag];
    const fixedUsed = FixedNumber.from(used);
    const fixedLimit = FixedNumber.from(limit);
    const usedPercent = (
      Number(fixedUsed.divUnsafe(fixedLimit).toString()) * 100
    ).toFixed(2);
    used = BigNumber.from(used);
    limit = BigNumber.from(limit);

    const amount = BigNumber.from(transaction.value as string);
    const gas = BigNumber.from(transaction.gas as string);
    const total = amount.add(gas);

    if (usageMsg === '') {
      usageMsg += `${tag}: ${usedPercent}%`;
    } else {
      usageMsg += ` | ${tag}: ${usedPercent}%`;
    }

    if (!limit.eq('0')) {
      if (used.gt(limit)) {
        alerts += `${EXCEEDED_MESSAGE + toEth(limit)} for the tag ${tag}. `;
      } else if (used.add(total).gte(limit)) {
        alerts += `${WILL_EXCEED_MESSAGE + toEth(limit)} for the tag ${tag}. `;
      }
    }
  }

  if (alerts === '') {
    return {
      Tag: tags,
      Usage: usageMsg + FOOTER_NOTE,
    };
  }

  return {
    Tag: tags,
    Usage: usageMsg + FOOTER_NOTE,
    Alerts: alerts,
  };
};
Copier

Ici, nous vérifions d'abord si le hash to (ou l'adresse to) de la transaction en cours est présent dans le stockage. Nous récupérons ensuite le pourcentage d'utilisation et le calculons. Nous vérifions ensuite si l'utilisation est sur le point d'atteindre, ou a déjà atteint, la limite définie pour cette étiquette, et nous envoyons une alerte appropriée.

Notez que nous utilisons les classes BigNumber et FixedNumber disponibles dans le package npm ethers pour gérer les opérations arithmétiques sur les grands nombres.

Les aperçus de transactions sont désormais complets, voici le résultat final :

Tâches cron

Trois tâches cron sont définies ici : weeklySummary pour générer le récapitulatif chaque semaine, checkLimits pour vérifier si l'utilisateur a dépassé les limites sur l'une de ses étiquettes, et updateAmount qui met à jour les informations d'utilisation des nouvelles transactions effectuées par l'utilisateur.

Ces tâches cron sont mentionnées dans packages\snap\snap.manifest.json, avec leur fréquence appropriée. De plus, un gestionnaire de tâches cron est intégré dans le fichier index.ts.

Ces fonctionnalités sont définies dans packages\snap\src\cron.ts.

export const getSummary = async (account: string, completeStorage: any) => {
  const storage = completeStorage[account];

  if (!storage.usage) {
    return null;
  }

  const { usage } = storage;
  const newUsage: any = {};
  let exceeded = false;
  let hasTag = false;

  for (const tag in usage) {
    if (Object.prototype.hasOwnProperty.call(usage, tag)) {
      hasTag = true;
      const { used } = usage[tag];
      const { limit } = usage[tag];

      newUsage[tag] = usage[tag];

      if (
        BigNumber.from(used).gt(BigNumber.from(limit)) &&
        BigNumber.from(limit).gt('0')
      ) {
        exceeded = true;
      }

      // reset usage information for the next week
      newUsage[tag].notified = false;
      newUsage[tag].used = '0';
    }
  }

  if (hasTag) {
    storage.usage = newUsage;
    completeStorage[account] = storage;
    await setPersistentStorage(completeStorage);
  }

  return exceeded;
};
Copier

Ici, nous parcourons le stockage et vérifions si l'utilisateur a dépassé les limites sur l'une des étiquettes. Nous réinitialisons ensuite notified à false afin qu'il puisse être réutilisé par la tâche cron checkLimits. Nous renvoyons une valeur booléenne pour que le gestionnaire de tâches cron la récupère pour chaque compte utilisateur et envoie un message de notification approprié en fin de semaine.

export const checkLimits = async (account: string, completeStorage: any) => {
  const storage = completeStorage[account];
  if (!storage.usage) {
    return null;
  }

  const { usage } = storage;
  const tags: string[] = [];
  const newUsage: any = {};

  for (const tag in usage) {
    if (Object.prototype.hasOwnProperty.call(usage, tag)) {
      const { used } = usage[tag];
      const { limit } = usage[tag];
      const { notified } = usage[tag];

      newUsage[tag] = usage[tag];
      if (
        !notified &&
        BigNumber.from(used).gt(BigNumber.from(limit)) &&
        BigNumber.from(limit).gt(BigNumber.from('0'))
      ) {
        tags.push(tag);
        newUsage[tag].notified = true;
      }
    }
  }

  if (tags.length > 0) {
    storage.usage = newUsage;
    completeStorage[account] = storage;
    await setPersistentStorage(completeStorage);

    const message = `${LIMIT_ALERT_HEADER + tags.length} tags on ${compact(
      account,
    )}`;
    return message;
  }

  return null;
};
Copier

Dans la fonction ci-dessus, nous parcourons à nouveau toutes les étiquettes utilisateur non notifiées et vérifions leur utilisation. Si elle a dépassé la limite définie, nous définissons notified à true afin que cette étiquette ne soit plus prise en compte jusqu'à la semaine suivante. Nous renvoyons un message au gestionnaire de tâches cron indiquant le nombre d'étiquettes ayant dépassé la limite et le hash du compte de l'utilisateur.

Ensuite, nous finalisons l'implémentation de updateAmount.

export const updateAmount = async (account: string, completeStorage: any) => {
  const response = await fetch(
    `https://api-goerli.etherscan.io/api?module=account&action=txlist&address=${account}&startblock=0&endblock=9999999999&sort=asc&apikey=${process.env.REACT_API_KEY}`,
  );
  const result = await response.json();

  if (!result.result) {
    return null;
  }

  let transactions = result.result;
  if (transactions.length === 0) {
    return null;
  }

  // sort in descending order
  transactions = transactions.sort(
    (a: any, b: any) => b.timeStamp - a.timeStamp,
  );

  const { latestHash } = completeStorage[account];
  const { prevHash } = completeStorage[account];
  if (transactions[0].hash.toLowerCase() === latestHash) {
    return null;
  }

  for (const transaction of transactions) {
    if (transaction.hash.toLowerCase() === latestHash) {
      break;
    }

    if (transaction.to !== account) {
      const toAddress = (transaction.to as string).toLowerCase();
      const tagList = completeStorage[account].mainMapping[toAddress];

      if (tagList !== null && tagList !== undefined) {
        for (const tag of tagList) {
          const gas = BigNumber.from(transaction.gasPrice).mul(
            BigNumber.from(transaction.gasUsed),
          );
          const value = BigNumber.from(transaction.value);
          const total = gas.add(value);
          if (prevHash !== latestHash) {
            completeStorage[account].usage[tag].used = BigNumber.from(
              completeStorage[account].usage[tag].used,
            )
              .add(total)
              .toString();
          } else {
            completeStorage[account].usage[tag].used = BigNumber.from('0')
              .add(total)
              .toString();
          }
        }
      }
    }
  }

  completeStorage[account].prevHash = latestHash;
  completeStorage[account].latestHash = transactions[0].hash.toLowerCase();

  return completeStorage;
};
Copier

Ici, nous utilisons l'API Etherscan pour récupérer toutes les transactions de l'utilisateur et mettre à jour les détails d'utilisation pour chaque étiquette. Nous trions les transactions obtenues par ordre décroissant de date et les analysons à partir de la transaction latestHash pour éviter de rescanner des transactions déjà traitées. Nous mettons enfin à jour latestHash. Cette tâche cron s'exécute plus fréquemment que checkLimits.

Voici quelques résultats des implémentations ci-dessus :

La DApp frontend

Fonctionnalité d'étiquetage

Le site web comporte deux pages. La première est la page d'accueil où l'utilisateur peut se connecter, ajouter/supprimer des étiquettes, consulter la répartition de l'utilisation de ses étiquettes, définir des limites et appliquer des filtres sur les étiquettes et les transactions. Tout cela est réalisé grâce aux fonctionnalités familières de React.js. Les transactions sont récupérées depuis l'API Etherscan mentionnée précédemment, et les adresses sont associées à leurs étiquettes correspondantes. Ces informations proviennent du stockage persistant des Snaps connectés.

La logique de la plupart des fonctionnalités ci-dessus est écrite dans packages\site\src\pages\GetTableData.jsx. L'intégration du snap avec le frontend est sophistiquée, mais intuitive une fois terminée.

Les méthodes du snap sont exposées au frontend en écrivant les fonctions suivantes dans packages\site\src\utils\snap.ts :

/**
 * Get the persisted data from the snap.
 *
 * @returns The persisted data, if any, 'null' otherwise.
 */
export const getStorage = async () => {
  return await window.ethereum.request({
    method: 'wallet_invokeSnap',
    params: [
      defaultSnapOrigin,
      {
        method: 'getPersistentStorage',
      },
    ],
  });
};

/**
 * Update the persistent storage in the snap.
 *
 * @param data - The complete data to be stored.
 */
export const setStorage = async (data: Record<string, unknown> | void) => {
  await window.ethereum.request({
    method: 'wallet_invokeSnap',
    params: [
      defaultSnapOrigin,
      {
        method: 'setPersistentStorage',
        params: data,
      },
    ],
  });
};

/**
 * Clear the persistent storage in the snap.
 */
export const clearStorage = async () => {
  return await window.ethereum.request({
    method: 'wallet_invokeSnap',
    params: [
      defaultSnapOrigin,
      {
        method: 'clearPersistentStorage',
      },
    ],
  });
};
Copier

Nous utilisons également le package npm react-chartjs-2 pour afficher les analyses des dépenses de l'utilisateur.

Voici l'apparence finale du site web :

Fonctionnalité de demande de montant

Il s'agit d'une fonctionnalité unique permettant à un utilisateur d'envoyer une notification via XMTP à un autre utilisateur pour lui demander des ETH. Pour cela, les deux utilisateurs doivent avoir ce snap activé. Cette fonctionnalité est disponible à l'adresse http://localhost:8000/request.

Pour ce faire, nous utilisons le package npm @xmtp/xmtp-js pour créer WalletContext et XmtpContext dans packages\site\src\contexts\WalletContext.tsx et packages\site\src\contexts\XmtpContext.tsx respectivement. Nous créons également des hooks et des composants. La majeure partie de ce code provient directement d'un exemple de la documentation.

Nous créons une nouvelle page, packages\site\src\pages\request.tsx, sur le site web pour accueillir l'interface de cette fonctionnalité. Nous envoyons des messages à l'aide de la fonction sendMessage du hook useSendMessage.

import useSendMessage from '../hooks/useSendMessage';

const sendNewMessage = () => {
    const payload = {
      id: Date.now(),
      message: msgTxt,
    };
    sendMessage(JSON.stringify(payload));
    setMsgTxt('');
};
Copier

Et nous affichons les messages reçus en utilisant l'état du provider de XmtpContext.

import { XmtpContext } from '../contexts/XmtpContext';

const Home = () => {
    const [providerState] = useContext(XmtpContext);
    const { convoMessages, client } = providerState;

    return (
        <>
            <ConversationList
                convoMessages={convoMessages}
                setSelectedConvo={setSelectedConvo}
            />
        </>
    )
};
Copier

Après avoir suivi l'exemple de la documentation mentionnée ci-dessus, nous obtenons finalement la fonctionnalité requise :

Conclusion

Cela complète FoxTagger, l'ensemble du projet avec notre idée concrétisée en une application MetaMask Snaps, couplée à une DApp.

Quelles seraient les prochaines étapes si vous deviez poursuivre ce projet ?

La prochaine étape serait d'étendre la fonctionnalité d'étiquetage en ajoutant davantage d'analyses et en utilisant un modèle de Machine Learning pour faire des prédictions et des suggestions sur la façon de minimiser les dépenses de l'utilisateur. Nous pourrions ensuite envisager d'intégrer une fonctionnalité de partage de transaction permettant à l'utilisateur de répartir des frais entre plusieurs comptes, en complément de la fonctionnalité de demande de montant.

De plus, la nouvelle fonctionnalité d'interface utilisateur personnalisée de MetaMask Snap pourrait être exploitée pour offrir une expérience utilisateur plus agréable à l'intérieur du snap. Enfin, le snap pourrait être étendu à d'autres chaînes que le réseau Goerli Testnet.

Pouvez-vous nous parler un peu de vous et de votre équipe ?

De l'idéation à la mise en œuvre, notre équipe a collaboré pour générer des idées, les affiner et surmonter les défis afin de créer nos Snaps FoxTagger.

Sachin Sahu a joué un rôle central dans le développement du frontend et des fonctionnalités de la DApp. Non seulement il a contribué à l'intégration de MetaMask Snaps avec la plateforme, mais il a également travaillé sur la démo et la documentation pour s'assurer que les utilisateurs disposent de toutes les informations nécessaires pour commencer à utiliser la DApp du Snap.

L'expertise de Siddhartha G en conception et implémentation frontend a été déterminante pour le développement de la plateforme. Il a créé des composants et des méthodes frontend intuitifs pour présenter et modifier les données, et a contribué à l'intégration de MetaMask Snaps avec la DApp.

Shreetesh M a pris en charge l'implémentation backend et s'est assuré que toutes les fonctions étaient exposées au frontend. Il a implémenté les aperçus de transactions et les tâches cron, ce qui a permis à l'équipe d'intégrer le backend de manière transparente avec le frontend.

Les contributions de Noble Saji Mathews ont été déterminantes dans l'élaboration du cadre pour les transactions via la messagerie on-chain. Il a également participé au processus d'idéation pour le système d'étiquetage et de demande de transaction, ce qui améliore la convivialité et l'ergonomie de la plateforme.

Kranthi a apporté sa perspective unique en proposant l'idée du système d'étiquetage et en explorant de nouvelles méthodes pour intégrer la communication décentralisée. Son travail acharné a abouti à l'implémentation de la fonctionnalité « request », qui a révolutionné la façon dont les utilisateurs interagissent avec la DApp du Snap.

L'expertise d'Ansh Anand en conception frontend et dans le processus d'idéation en a fait un membre inestimable de l'équipe. Il a travaillé sur la vidéo de démonstration, qui a mis en valeur les capacités de la plateforme et contribué à attirer davantage d'utilisateurs.

Quelles opportunités voyez-vous avec MetaMask Snaps et les possibilités qu'il offre pour l'espace Web3 ?

Avec plus de 21 millions d'utilisateurs actifs sur MetaMask, il est assez difficile de proposer des fonctionnalités adaptées aux besoins de chacun. C'est là qu'interviennent les MetaMask Snaps : la communauté de développeurs dispose désormais d'une plus grande liberté pour personnaliser MetaMask en fonction des besoins des utilisateurs, rendant ainsi l'interaction avec différentes DApps — qu'elles soient construites sur différentes blockchains ou qu'elles utilisent différents protocoles — plus simple que jamais.

Les avantages de MetaMask Snaps sont nombreux : possibilité de suivre les dépenses dans le temps, d'analyser les dépenses, de faire un don d'un petit pourcentage de chaque transaction à une œuvre caritative, de demander un paiement à des clients, de configurer un paiement automatique pour des abonnements Netflix, et bien plus encore ! L'intégration de MetaMask avec divers protocoles DeFi, places de marché NFT et plateformes de réseaux sociaux est désormais un jeu d'enfant grâce à MetaMask Snaps.

Mais les Snaps ne sont pas réservés aux transactions financières complexes. Imaginez aller déjeuner avec des amis et payer en cryptomonnaie — avec MetaMask Snaps, partager l'addition et demander un remboursement à vos amis est aussi simple qu'un seul tap ! C'est la puissance de MetaMask Snaps !

À mesure que l'espace Web3 continue d'évoluer, MetaMask Snaps est appelé à devenir un outil de plus en plus important pour les développeurs comme pour les utilisateurs. Avec le potentiel du métavers à l'horizon, les cas d'usage des Snaps sont véritablement illimités.

Avez-vous des conseils à partager avec les développeurs souhaitant essayer MetaMask Snaps ?

Nous leur conseillons de commencer par parcourir les snaps existants sur GitHub. Cela les aidera beaucoup à comprendre le potentiel des fonctionnalités offertes par MetaMask Snaps, même si ces fonctionnalités semblent simples sur le papier.

Nous leur suggérons également d'utiliser le modèle monorepo existant pour démarrer leur implémentation. Il présente de nombreux avantages : le dépôt est très bien structuré, avec des vérifications de linting et des actions, les développeurs peuvent implémenter à la fois une application web compagnon et un snap dans un seul package. De plus, le dépôt est toujours en phase avec la documentation officielle.

Enfin, nous leur recommandons de publier leurs problèmes ou questions sur les canaux communautaires et d'accroître leur exposition à MetaMask Snaps.

Développer avec MetaMask Snaps

Pour commencer avec MetaMask Snaps :

  1. Consultez la documentation développeur

  2. Installez MetaMask Flask

  3. Consultez un guide MetaMask Snaps

  4. Restez connecté avec nous sur Twitter, les discussions GitHub et Discord

Gardez un œil sur notre équipe lors du prochain hackathon près de chez vous ! Bonne construction ⚒️

Avertissement : Les MetaMask Snaps sont généralement développés par des tiers autres que Consensys Software. L'utilisation de MetaMask Snaps développés par des tiers se fait à votre propre discrétion et à vos propres risques, et implique votre accord selon lequel vous serez seul responsable de toute perte ou dommage résultant de ces activités. Consensys ne donne aucune garantie expresse ou implicite, qu'elle soit orale ou écrite, concernant les MetaMask Snaps développés par des tiers, et décline toute responsabilité pour les Snaps développés par des tiers. L'utilisation de logiciels liés à la blockchain comporte des risques que vous assumez entièrement lors de l'utilisation de MetaMask Snaps.

Traduit par IA. Peut contenir des erreurs. Veuillez toujours vérifier les informations.

Évaluer la traduction

Cet article a été rédigé par :

  • MetaMask

    MetaMask, anciennement Consensys Software Inc, est la plus grande plateforme financière self-custodial au monde, offrant aux utilisateurs un espace unique pour détenir, dépenser, épargner et faire fructifier leur argent, aussi bien en crypto qu'en actifs traditionnels. La société développe la plateforme grand public au sein de laquelle tout cela se concrétise, en réunissant paiements, épargne, investissement et actifs numériques dans une expérience fluide et unifiée. Issu du wallet self-custodial le plus utilisé au monde, MetaMask donne aux utilisateurs un contrôle direct sur leur argent et leurs actifs, avec une présence dans environ 190 pays. MetaMask joue un rôle fondateur dans la croissance d'Ethereum depuis 2016. Aujourd'hui, MetaMask occupe une place centrale dans l'économie blockchain, en construisant le système d'exploitation de l'Open Money et en donnant aux individus un contrôle total sur leur vie financière.

    Lire tous les articles