FoxTagger Snap: Mapeo de direcciones con etiquetas definidas por el usuario

Ganador del Hackathon InterIIT

12 min de lectura
FoxTagger Snap: Mapeo de direcciones con etiquetas definidas por el usuario

MetaMask Snaps es la hoja de ruta para convertir a MetaMask en la wallet más extensible del mundo. Como desarrollador, puedes incorporar tus funcionalidades y APIs a MetaMask de formas completamente nuevas. Los desarrolladores de Web3 son el núcleo de este crecimiento, y esta serie tiene como objetivo destacar los novedosos MetaMask Snaps que se están construyendo hoy en día.

FoxTagger Snap

Repositorio del Snap: https://github.com/shree675/FoxTagger

FoxTagger es una herramienta que facilita la asignación de etiquetas definidas por el usuario a direcciones, con el fin de ayudar a los usuarios a controlar sus gastos mediante alertas y la visualización de análisis de uso.

¿Puedes explicarnos la implementación técnica?

La Configuración Técnica

Nuestra extensión de MetaMask, FoxTagger, consta de dos componentes principales: un backend de MetaMask Snaps (también conocido como el snap) y un frontend de Gatsby.js. Si bien el frontend es una aplicación web sencilla que aloja la interfaz de usuario, también actúa como DApp complementaria para nuestra aplicación Snaps.

La mayor parte de la funcionalidad se implementa en el snap, pero el resto se encuentra en el frontend para permitir que los usuarios soliciten montos a otros y/o configuren sus etiquetas a través de nuestra interfaz. Mientras que la función de solicitud de monto utiliza el protocolo XMTP, la función de etiquetado utiliza la API de MetaMask Snaps.

Hemos utilizado el snap monorepo como plantilla de partida. La implementación completa de nuestro proyecto se puede encontrar aquí.

La Aplicación Snaps

En toda nuestra implementación, hacemos uso de las siguientes funcionalidades principales que ofrece la API de Snaps:

  1. Almacenamiento persistente

  2. Notificaciones

  3. Cron jobs

  4. Transaction insights

Nuestra implementación comienza exponiendo algunas funciones al frontend en 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.');
  }
};
Copiar

Las funciones getPersistenceStorage y setPersistenceStorage son fundamentales, ya que facilitan el almacenamiento y la recuperación de etiquetas y otra información, y están definidas como se muestra en el siguiente fragmento de código.

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],
  });
};
Copiar

Para todas las implementaciones de funcionalidades, hemos creado una estructura de datos adecuada para nuestro almacenamiento persistente.

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

Un usuario puede tener múltiples cuentas, y cada cuenta tiene sus propias etiquetas, uso y límites definidos por el usuario.

Transaction Insights

La funcionalidad de transaction insights muestra las etiquetas asociadas, sus gastos correspondientes y alertas, si las hubiera, durante una transacción en curso.

En el mismo archivo index.ts, añadimos un manejador de transaction insights que nos permite interceptar una transacción en curso e interactuar con la wallet de MetaMask.

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

  return {
    insights,
  };
};
Copiar

La función getDetails implementa toda la lógica para calcular el gasto y mostrar alertas. Esto está implementado en 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,
  };
};
Copiar

En este caso, primero verificamos si el hash to (o dirección to) de la transacción actual está presente en el almacenamiento. Luego recuperamos y calculamos el porcentaje de uso. A continuación, comprobamos si el uso está a punto de alcanzar, o ya ha alcanzado, el límite establecido para esa etiqueta y enviamos una alerta correspondiente.

Nótese que utilizamos las clases BigNumber y FixedNumber disponibles en el paquete npm ethers para gestionar operaciones aritméticas con números grandes.

Transaction insights está ahora completo y este es el resultado final:

Cron Jobs

Aquí se definen tres cron jobs: weeklySummary para generar el resumen semanal, checkLimits para verificar si el usuario ha superado los límites en alguna de sus etiquetas, y updateAmount que actualiza la información de uso de las nuevas transacciones realizadas por el usuario.

Estos cron jobs se mencionan en packages\snap\snap.manifest.json, con su frecuencia correspondiente. Además, se incorpora un manejador de cron jobs en el archivo index.ts.

Estas funcionalidades están definidas en 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;
};
Copiar

Aquí, iteramos a través del almacenamiento y verificamos si el usuario ha superado algún límite en alguna de las etiquetas. Luego restablecemos notified a false para que pueda ser reutilizado por el cron job checkLimits. Devolvemos un valor booleano para que el manejador del cron job lo recoja por cada cuenta de usuario y envíe un mensaje de notificación apropiado al final de la semana.

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;
};
Copiar

En la función anterior, iteramos nuevamente por todas las etiquetas de usuario no notificadas y verificamos su uso. Si ha superado el límite establecido, configuramos notified como true para que esta etiqueta no sea procesada de nuevo hasta la semana siguiente. Devolvemos un mensaje al manejador del cron job indicando el número de etiquetas que superaron el límite y el hash de la cuenta del usuario.

A continuación, completamos la implementación 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;
};
Copiar

Aquí utilizamos la API de Etherscan para obtener todas las transacciones del usuario y actualizar los detalles de uso de cada etiqueta. Ordenamos las transacciones obtenidas en orden decreciente de tiempo y las analizamos desde la transacción latestHash para evitar el reprocesamiento de transacciones. Finalmente, actualizamos latestHash. Este cron job se ejecuta con mayor frecuencia que checkLimits.

A continuación se muestran algunos resultados de las implementaciones anteriores:

La DApp Frontend

Funcionalidad de Etiquetado

El sitio web tiene dos páginas. La primera es la página de inicio donde el usuario puede iniciar sesión, añadir/eliminar etiquetas, consultar la distribución de uso de sus etiquetas, establecer límites y aplicar filtros sobre las etiquetas y transacciones. Todo esto se logra mediante la conocida funcionalidad de React.js. Las transacciones se obtienen de la API de Etherscan mencionada anteriormente y las direcciones se asocian con sus etiquetas correspondientes. Esta información proviene del almacenamiento persistente de los Snaps conectados.

La lógica para la mayoría de las funcionalidades anteriores está escrita en packages\site\src\pages\GetTableData.jsx. La integración del snap con el frontend es sofisticada, aunque resulta intuitiva una vez finalizada.

Los métodos del snap se exponen al frontend escribiendo las siguientes funciones en 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',
      },
    ],
  });
};
Copiar

También utilizamos el paquete npm react-chartjs-2 para mostrar los análisis de gasto del usuario.

Este es el aspecto final del sitio web:

Funcionalidad de Solicitud de Monto

Esta es una funcionalidad única en la que un usuario puede enviar una notificación a través de XMTP a otro usuario solicitando ETH. Para ello, ambos usuarios deben tener este snap habilitado. Esta funcionalidad está disponible en http://localhost:8000/request.

Para esto, utilizamos el paquete npm @xmtp/xmtp-js para crear WalletContext y XmtpContext en packages\site\src\contexts\WalletContext.tsx y packages\site\src\contexts\XmtpContext.tsx respectivamente. También creamos hooks y componentes. La mayor parte de este código proviene directamente de un ejemplo en la documentación.

Creamos una nueva página, packages\site\src\pages\request.tsx en el sitio web para alojar la interfaz de usuario de esta funcionalidad. Enviamos mensajes utilizando la función sendMessage del hook useSendMessage.

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

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

Y mostramos los mensajes recibidos utilizando el estado del proveedor de XmtpContext.

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

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

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

Tras seguir el ejemplo de la documentación mencionada anteriormente, finalmente logramos la funcionalidad requerida:

Conclusión

Con esto concluye FoxTagger, el proyecto completo con nuestra idea materializada en una aplicación de MetaMask Snaps, combinada con una DApp.

¿Cuáles serían los próximos pasos si continuaras con este proyecto?

El siguiente paso sería ampliar la funcionalidad de etiquetado añadiendo más análisis y utilizando un modelo de Machine Learning para realizar predicciones y sugerencias sobre cómo minimizar el gasto del usuario. Luego podríamos apuntar a incorporar una funcionalidad de división de transacciones, donde el usuario pueda repartir una comisión entre múltiples cuentas, complementando la funcionalidad de solicitud de monto.

Además, la nueva funcionalidad de interfaz de usuario personalizada de MetaMask Snap podría aprovecharse para ofrecer una experiencia de usuario más agradable dentro del snap. Por último, el snap podría extenderse a cadenas distintas a la red Goerli Testnet.

¿Puedes contarnos un poco sobre ti y tu equipo?

Desde la ideación hasta la implementación, nuestro equipo colaboró para generar ideas, perfeccionarlas y superar desafíos para dar vida a nuestro FoxTagger Snaps.

Sachin Sahu desempeñó un papel fundamental en el desarrollo del frontend y las funcionalidades de la dApp. No solo ayudó a integrar MetaMask Snaps con la plataforma, sino que también trabajó en la demo y la documentación para garantizar que los usuarios cuenten con toda la información necesaria para comenzar a usar la dApp del Snap.

La experiencia de Siddhartha G en diseño e implementación frontend fue crucial para el desarrollo de la plataforma. Creó componentes y métodos frontend intuitivos para presentar y modificar datos, y colaboró en la integración de MetaMask Snaps con la dApp.

Shreetesh M se encargó de la implementación del backend y se aseguró de que todas las funciones estuvieran expuestas al frontend. Implementó los transaction insights y los cron jobs, lo que ayudó al equipo a integrar el backend de forma fluida con el frontend.

Las contribuciones de Noble Saji Mathews fueron fundamentales para diseñar el marco de las transacciones a través de mensajería on-chain. También participó en el proceso de ideación del sistema de etiquetado y solicitud de transacciones, lo que mejora la usabilidad y la facilidad de uso de la plataforma.

Kranthi aportó su perspectiva única al proponer la idea del sistema de etiquetado y explorar nuevos métodos para incorporar la comunicación descentralizada. Su arduo trabajo resultó en la implementación de la funcionalidad de "solicitud", que ha revolucionado la forma en que los usuarios interactúan con la dApp del Snap.

La experiencia de Ansh Anand en diseño frontend y en el proceso de ideación lo convirtió en un miembro invaluable del equipo. Trabajó en el video de demostración, que mostró las capacidades de la plataforma y ayudó a atraer a más usuarios.

¿Qué oportunidades ves con MetaMask Snaps y las posibilidades que abre para el espacio Web3?

Con más de 21 millones de usuarios activos en MetaMask, resulta bastante difícil ofrecer una funcionalidad que se adapte a las necesidades de todos. Ahí es donde entran en juego los MetaMask Snaps: la comunidad de desarrolladores ahora tiene mayor libertad para personalizar MetaMask según los requisitos de los usuarios, haciendo que interactuar con diferentes dApps construidas sobre distintas blockchains o que utilizan diferentes protocolos sea más fácil que nunca.

Los beneficios de MetaMask Snaps son numerosos: la posibilidad de rastrear gastos a lo largo del tiempo, analizar el gasto, donar un pequeño porcentaje de cada transacción a organizaciones benéficas, solicitar pagos a clientes, configurar pagos automáticos para suscripciones de Netflix, ¡y mucho más! Integrar MetaMask con diversos protocolos DeFi, marketplaces de NFT y plataformas de redes sociales es ahora muy sencillo gracias a MetaMask Snaps.

¡Pero los Snaps no son solo para transacciones financieras complejas! Imagina salir a almorzar con amigos y pagar con criptomonedas: con MetaMask Snaps, dividir la cuenta y solicitar el pago a tus amigos es tan simple como un solo toque. ¡Ese es el poder de MetaMask Snaps!

A medida que el espacio Web3 continúa evolucionando, MetaMask Snaps se convertirá sin duda en una herramienta cada vez más importante tanto para desarrolladores como para usuarios. Con el potencial del metaverso en el horizonte, los casos de uso de los Snaps son verdaderamente ilimitados.

¿Algún consejo que quieras compartir con los desarrolladores interesados en probar MetaMask Snaps?

Les recomendaríamos que primero revisen los snaps existentes en GitHub. Esto les ayudará enormemente a comprender el potencial de las funcionalidades que ofrece MetaMask Snaps, aunque dichas funcionalidades parezcan sencillas sobre el papel.

También les sugerimos que utilicen la plantilla del monorepo existente como punto de partida para su implementación. Tiene muchas ventajas: el repositorio está muy bien estructurado, incluye verificaciones de linting y acciones, los desarrolladores pueden implementar tanto una aplicación web complementaria como un snap en un solo paquete. Además, el repositorio siempre está alineado con la documentación oficial.

Por último, les recomendamos que publiquen sus problemas o dudas en los canales de la comunidad y amplíen así su exposición a los Snaps de MetaMask.

Construyendo con MetaMask Snaps

Para comenzar con MetaMask Snaps:

  1. Consulta la documentación para desarrolladores

  2. Instala MetaMask Flask

  3. Revisa una guía de MetaMask Snaps

  4. Mantente conectado con nosotros en Twitter, GitHub discussions y Discord

¡Estate atento a nuestro equipo en el próximo hackathon cerca de ti! ¡Feliz BUIDLing! ⚒️

Aviso legal: Los MetaMask Snaps son generalmente desarrollados por terceros distintos a Consensys Software. El uso de MetaMask Snaps desarrollados por terceros se realiza bajo tu propio criterio y riesgo, y con el acuerdo de que serás el único responsable de cualquier pérdida o daño que resulte de dichas actividades. Consensys no ofrece ninguna garantía expresa ni implícita, ya sea verbal o escrita, con respecto a los MetaMask Snaps desarrollados por terceros, y declina toda responsabilidad por los Snaps desarrollados por terceros. El uso de software relacionado con blockchain conlleva riesgos, los cuales asumes en su totalidad al utilizar MetaMask Snaps.

Traducido por IA. Puede contener errores. Por favor, verifique siempre la información.

Califica la traducción
  • MetaMask
    MetaMask

    La billetera cripto autocustodiada líder y puerta de entrada a Web3, desarrollada por Consensys.

    Leer todos los artículos