Pular para o conteúdo

FoxTagger Snap: Mapeamento de endereços com tags definidas pelo usuário

Vencedor do InterIIT Hackathon

12 min de leitura
FoxTagger Snap: Mapeamento de endereços com tags definidas pelo usuário

MetaMask Snaps é o roteiro para tornar a MetaMask a carteira mais extensível do mundo. Como desenvolvedor, você pode trazer seus recursos e APIs para a MetaMask de formas totalmente novas. Os desenvolvedores Web3 são o núcleo desse crescimento, e esta série tem como objetivo destacar os MetaMask Snaps inovadores que estão sendo criados hoje.

FoxTagger Snap

Repositório do Snap: https://github.com/shree675/FoxTagger

O FoxTagger é uma ferramenta que facilita o mapeamento de endereços com tags definidas pelo usuário para ajudá-lo a controlar seus gastos, emitindo alertas e exibindo análises de uso.

Nos explique a implementação técnica?

A Configuração Técnica

Nossa extensão MetaMask, FoxTagger, é composta por dois componentes principais: um backend MetaMask Snaps (também conhecido como o snap) e um frontend em Gatsby.js. Enquanto o frontend é uma aplicação web simples que hospeda a interface do usuário, ele também funciona como um DApp complementar para nossa aplicação Snaps.

A maior parte da funcionalidade é implementada no snap, mas o restante está no frontend para permitir que os usuários solicitem valores de outros usuários e/ou configurem suas tags por meio da nossa interface. Enquanto o recurso de solicitação de valor utiliza o protocolo XMTP, o recurso de tagging usa a MetaMask Snaps API.

Utilizamos o snap monorepo como template inicial. A implementação completa do nosso projeto pode ser encontrada aqui.

A Aplicação Snaps

Em toda a nossa implementação, utilizamos os seguintes recursos principais fornecidos pela Snaps API:

  1. Armazenamento persistente

  2. Notificações

  3. Cron jobs

  4. Transaction insights

Nossa implementação começa expondo algumas funções ao frontend em 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

As funções getPersistenceStorage e setPersistenceStorage são fundamentais, pois facilitam o armazenamento e a recuperação de tags e outras informações, e são definidas conforme mostrado no trecho de código abaixo.

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 as implementações de recursos, criamos uma estrutura de dados adequada para nosso armazenamento persistente.

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

Um usuário pode ter múltiplas contas, e cada conta possui suas próprias tags, histórico de uso e limites definidos pelo usuário.

Transaction Insights

O recurso de transaction insights exibe as tags associadas, seus respectivos gastos e alertas, se houver, em uma transação em andamento.

No mesmo arquivo index.ts, adicionamos um handler de transaction insights que nos permite interceptar uma transação em andamento e interagir com a carteira MetaMask.

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

  return {
    insights,
  };
};
Copiar

A função getDetails implementa toda a lógica para calcular os gastos e exibir alertas. Ela está implementada em 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

Nela, primeiro verificamos se o hash to (ou endereço to) da transação atual está presente no armazenamento. Em seguida, recuperamos e calculamos o percentual de uso. Depois, verificamos se o uso está prestes a atingir, ou já atingiu, o limite definido para aquela tag e enviamos um alerta adequado.

Observe que usamos as classes BigNumber e FixedNumber disponíveis no pacote npm ethers para lidar com operações aritméticas em números grandes.

O transaction insights está concluído e este é o resultado final:

Cron Jobs

Aqui são definidos três cron jobs: weeklySummary para gerar o resumo semanal, checkLimits para verificar se o usuário ultrapassou os limites em alguma de suas tags, e updateAmount que atualiza as informações de uso das novas transações realizadas pelo usuário.

Esses cron jobs são mencionados em packages\snap\snap.manifest.json, com suas respectivas frequências. Além disso, um handler de cron job é incorporado no arquivo index.ts.

Essas funcionalidades são definidas em 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

Aqui, iteramos pelo armazenamento e verificamos se o usuário ultrapassou algum limite em qualquer uma das tags. Em seguida, redefinimos notified para false para que possa ser reutilizado pelo cron job checkLimits. Retornamos um valor booleano para que o handler do cron job o capture para cada conta de usuário e envie uma mensagem de notificação adequada ao final da 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

Na função acima, iteramos novamente por todas as tags de usuário não notificadas e verificamos seu uso. Se ele tiver excedido o limite definido, definimos notified como true para que essa tag não seja processada novamente até a próxima semana. Retornamos uma mensagem ao handler do cron job informando o número de tags que ultrapassaram o limite e o hash da conta do usuário.

Em seguida, concluímos a implementação 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

Aqui, usamos a API do Etherscan para buscar todas as transações do usuário e atualizar os detalhes de uso para cada tag. Ordenamos as transações obtidas em ordem decrescente de tempo e as varremos a partir da transação latestHash para evitar o reprocessamento de transações. Por fim, atualizamos latestHash. Este cron job é executado com mais frequência do que checkLimits.

Aqui estão alguns resultados das implementações acima:

O DApp Frontend

Recurso de Tagging

O site possui duas páginas. A primeira é a página inicial, onde o usuário pode fazer login, adicionar/remover tags, verificar a distribuição de uso de suas tags, definir limites e aplicar filtros nas tags e transações. Tudo isso é realizado por meio da funcionalidade familiar do React.js. As transações são obtidas da API do Etherscan mencionada anteriormente, e os endereços são associados às suas tags correspondentes. Essas informações vêm do armazenamento persistente dos Snaps conectados.

A lógica para a maioria dos recursos acima está escrita em packages\site\src\pages\GetTableData.jsx. A integração do snap com o frontend é sofisticada, mas intuitiva após concluída.

Os métodos do snap são expostos ao frontend por meio das seguintes funções escritas em 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

Também utilizamos o pacote npm react-chartjs-2 para exibir análises dos gastos do usuário.

Esta é a aparência final do site:

Recurso de Solicitação de Valor

Este é um recurso exclusivo no qual um usuário pode enviar uma notificação via XMTP para outro usuário solicitando ETH. Para isso, ambos os usuários precisam ter este snap habilitado. Este recurso está disponível em http://localhost:8000/request.

Para isso, usamos o pacote npm @xmtp/xmtp-js para criar WalletContext e XmtpContext em packages\site\src\contexts\WalletContext.tsx e packages\site\src\contexts\XmtpContext.tsx, respectivamente. Também criamos hooks e componentes. Grande parte desse código vem diretamente de um exemplo na documentação.

Criamos uma nova página, packages\site\src\pages\request.tsx, no site para acomodar a interface desse recurso. Enviamos mensagens usando a função sendMessage do hook useSendMessage.

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

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

E exibimos as mensagens recebidas usando o estado do provider do XmtpContext.

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

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

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

Após seguir o exemplo na documentação mencionada acima, finalmente obtemos a funcionalidade desejada:

Conclusão

Isso conclui o FoxTagger, o projeto completo com nossa ideia materializada em uma aplicação MetaMask Snaps, combinada com um DApp.

Quais seriam os próximos passos se você fosse implementar isso?

O próximo passo seria expandir a funcionalidade de tagging adicionando mais análises e utilizando um modelo de Machine Learning para fazer previsões e sugestões sobre como minimizar os gastos do usuário. Em seguida, poderíamos incorporar um recurso de divisão de transações, onde o usuário pode dividir uma taxa entre múltiplas contas, complementando o recurso de solicitação de valor.

Além disso, a nova funcionalidade de UI personalizada do MetaMask Snap poderia ser aproveitada para oferecer uma experiência de usuário mais agradável dentro do snap. Por fim, o snap pode ser expandido para outras redes além da Goerli Testnet.

Pode nos contar um pouco sobre você e sua equipe?

Da ideação à implementação, nossa equipe colaborou para gerar ideias, refiná-las e superar desafios para criar nosso FoxTagger Snaps.

Sachin Sahu desempenhou um papel fundamental no desenvolvimento do frontend e das funcionalidades do dApp. Além de ajudar a integrar o MetaMask Snaps à plataforma, ele também trabalhou na demonstração e na documentação para garantir que os usuários tenham todas as informações necessárias para começar a usar o dApp do Snap.

A expertise de Siddhartha G em design e implementação de frontend foi crucial para o desenvolvimento da plataforma. Ele criou componentes e métodos de frontend intuitivos para apresentar e modificar dados, além de auxiliar na integração do MetaMask Snaps com o dApp.

Shreetesh M assumiu a responsabilidade pela implementação do backend e garantiu que todas as funções fossem expostas ao frontend. Ele implementou os transaction insights e os cron jobs, o que ajudou a equipe a integrar o backend de forma fluida com o frontend.

As contribuições de Noble Saji Mathews foram fundamentais na elaboração do framework para transações por meio de mensagens on-chain. Ele também participou do processo de ideação para o sistema de tagging e solicitação de transações, o que aprimora a usabilidade e a facilidade de uso da plataforma.

Kranthi trouxe sua perspectiva única ao propor a ideia do sistema de tagging e explorar novos métodos para incorporar comunicação descentralizada. Seu trabalho árduo resultou na implementação do recurso de 'solicitação', que revolucionou a forma como os usuários interagem com o dApp do Snap.

A expertise de Ansh Anand em design de frontend e no processo de ideação o tornou um membro inestimável da equipe. Ele trabalhou no vídeo de demonstração, que apresentou as capacidades da plataforma e ajudou a atrair mais usuários.

Que oportunidades você enxerga com o MetaMask Snaps e as possibilidades que ele abre para o espaço Web3?

Com mais de 21 milhões de usuários ativos na MetaMask, é bastante difícil oferecer funcionalidades que atendam às necessidades de todos. É aí que entram os MetaMask Snaps — a comunidade de desenvolvedores agora tem mais liberdade para personalizar a MetaMask de acordo com as necessidades dos usuários, tornando mais fácil do que nunca interagir com diferentes dApps que podem ser construídos em diferentes blockchains ou usar diferentes protocolos.

Os benefícios do MetaMask Snaps são inúmeros, incluindo a capacidade de rastrear gastos ao longo do tempo, analisar despesas, doar uma pequena porcentagem de cada transação para caridade, solicitar pagamento de clientes, configurar pagamento automático para assinaturas da Netflix e muito mais! Integrar a MetaMask com diversos protocolos DeFi, marketplaces de NFT e plataformas de redes sociais agora é muito simples com o MetaMask Snaps.

Mas os Snaps não são apenas para transações financeiras complexas. Imagine sair para almoçar com amigos e pagar com criptomoeda — com o MetaMask Snaps, dividir a conta e solicitar pagamento dos seus amigos é tão simples quanto um único toque! Esse é o poder do MetaMask Snaps!

À medida que o espaço Web3 continua a evoluir, o MetaMask Snaps certamente se tornará uma ferramenta cada vez mais importante tanto para desenvolvedores quanto para usuários. Com o potencial do metaverso no horizonte, os casos de uso dos Snaps são verdadeiramente ilimitados.

Algum conselho que gostaria de compartilhar com desenvolvedores interessados em experimentar o MetaMask Snaps?

Aconselhamos que primeiro explorem os snaps existentes no GitHub. Isso os ajudará muito a entender o potencial dos recursos fornecidos pelo MetaMask Snaps, embora os recursos pareçam simples no papel.

Também sugerimos que utilizem o template do monorepo existente para iniciar sua implementação. Ele oferece muitos benefícios: o repositório é muito bem estruturado, com verificações de linting e actions; os desenvolvedores podem implementar tanto um web app complementar quanto um snap em um único pacote. Além disso, o repositório está sempre alinhado com a documentação oficial.

Por fim, recomendamos que publiquem suas dúvidas ou questionamentos nos canais da comunidade e ampliem sua exposição aos Snaps da MetaMask.

Desenvolvendo com MetaMask Snaps

Para começar com o MetaMask Snaps:

  1. Confira a documentação para desenvolvedores

  2. Instale o MetaMask Flask

  3. Confira um guia do MetaMask Snaps

  4. Fique conectado conosco no Twitter, nas discussões do GitHub e no Discord

Fique de olho em nossa equipe no próximo hackathon perto de você! Bom BUIDL ⚒️

Aviso legal: Os MetaMask Snaps são geralmente desenvolvidos por terceiros que não a Consensys Software. O uso de MetaMask Snaps desenvolvidos por terceiros é feito a seu próprio critério e risco, e com o entendimento de que você será o único responsável por qualquer perda ou dano resultante de tais atividades. A Consensys não oferece nenhuma garantia expressa ou implícita, seja verbal ou escrita, em relação a MetaMask Snaps desenvolvidos por terceiros, e se isenta de toda responsabilidade por Snaps desenvolvidos por terceiros. O uso de software relacionado a blockchain envolve riscos, e você os assume integralmente ao utilizar o MetaMask Snaps.

Traduzido por IA. Pode conter erros. Por favor, verifique sempre as informações.

Avalie a tradução

Este artigo foi escrito por:

  • MetaMask

    A MetaMask, anteriormente Consensys Software Inc, é a maior plataforma financeira self-custodial do mundo, oferecendo às pessoas um único lugar para guardar, gastar, economizar e fazer seu dinheiro crescer em ativos cripto e tradicionais. A empresa está construindo a plataforma de consumo onde isso acontece, reunindo pagamentos, poupança, investimentos e ativos digitais em uma experiência integrada e fluida. Tendo crescido a partir da carteira self-custodial mais amplamente utilizada do mundo, a MetaMask oferece aos usuários controle direto sobre seu dinheiro e ativos, com alcance em aproximadamente 190 países. A MetaMask desempenhou um papel fundamental no crescimento do Ethereum desde 2016. Hoje, a MetaMask está no centro da economia blockchain, construindo o sistema operacional para o Open Money e colocando as pessoas em total controle de suas vidas financeiras.

    Ler todos os artigos