Перейти к содержимому

FoxTagger Snap: сопоставление адресов с пользовательскими тегами

Победитель хакатона InterIIT

12 мин чтения
FoxTagger Snap: сопоставление адресов с пользовательскими тегами

MetaMask Snaps — это дорожная карта к превращению MetaMask в самый расширяемый кошелёк в мире. Как разработчик, вы можете интегрировать свои функции и API в MetaMask совершенно новыми способами. Разработчики Web3 — это основа этого роста, и данная серия материалов призвана продемонстрировать новаторские MetaMask Snaps, создаваемые сегодня.

FoxTagger Snap

Репозиторий Snap: https://github.com/shree675/FoxTagger

FoxTagger — это инструмент, который позволяет сопоставлять адреса с пользовательскими тегами, помогая пользователям контролировать расходы с помощью оповещений и отображения аналитики использования.

Расскажите нам о технической реализации?

Техническая архитектура

Наше расширение MetaMask, FoxTagger, состоит из двух основных компонентов: бэкенда MetaMask Snaps (он же snap) и фронтенда на Gatsby.js. Фронтенд представляет собой простое веб-приложение, которое размещает пользовательский интерфейс, а также выступает в роли companion DApp для нашего приложения Snaps.

Большая часть функциональности реализована в snap, а остальное — во фронтенде, чтобы пользователи могли запрашивать суммы у других и/или настраивать свои теги через наш интерфейс. Функция запроса суммы использует протокол XMTP, а функция тегирования — MetaMask Snaps API.

В качестве шаблона мы использовали snap monorepo. Полная реализация нашего проекта доступна здесь.

Приложение Snaps

В нашей реализации мы используем следующие основные функции, предоставляемые Snaps API:

  1. Постоянное хранилище (Persistence storage)

  2. Уведомления

  3. Cron-задачи

  4. Transaction insights

Реализация начинается с открытия ряда функций для фронтенда в 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.');
  }
};
Копировать

Функции getPersistenceStorage и setPersistenceStorage являются фундаментальными, поскольку обеспечивают хранение и получение тегов и другой информации. Они определены так, как показано в приведённом ниже фрагменте кода.

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],
  });
};
Копировать

Для всех реализованных функций мы создали подходящую структуру данных для нашего постоянного хранилища.

{
    "from_account0": {
      mainMapping: {
        "to_account0": ["tag0","tag1"],
        ...
      },
      usage: {
        "tag0": {
          limit: "100000000000",
          used: "800000",
          notified: false
        },
        ...
      },
      latestHash: "transaction_hash0"
    },
    ...
  }
Копировать

У пользователя может быть несколько аккаунтов, и у каждого аккаунта есть собственные теги, история использования и пользовательские лимиты.

Transaction Insights

Функция transaction insights отображает связанные теги, соответствующие расходы и оповещения (при наличии) в рамках текущей транзакции.

В том же файле index.ts мы добавляем обработчик transaction insights, который позволяет перехватывать текущую транзакцию и взаимодействовать с кошельком MetaMask.

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

  return {
    insights,
  };
};
Копировать

Функция getDetails реализует всю логику расчёта расходов и отображения оповещений. Она реализована в 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,
  };
};
Копировать

Здесь мы сначала проверяем, присутствует ли хэш to (или адрес to) текущей транзакции в хранилище. Затем мы получаем и рассчитываем процент использования. После этого проверяем, приближается ли использование к установленному лимиту для данного тега или уже превысило его, и отправляем соответствующее оповещение.

Обратите внимание, что мы используем классы BigNumber и FixedNumber из npm-пакета ethers для выполнения арифметических операций с большими числами.

Реализация transaction insights завершена, и вот конечный результат:

Cron-задачи

Здесь определены три cron-задачи: weeklySummary — для формирования еженедельной сводки, checkLimits — для проверки превышения лимитов по любому из тегов пользователя, и updateAmount — для обновления информации об использовании по новым транзакциям пользователя.

Эти cron-задачи указаны в packages\snap\snap.manifest.json с соответствующей частотой запуска. Кроме того, обработчик cron-задач включён в файл index.ts.

Данные функции определены в 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;
};
Копировать

Здесь мы перебираем хранилище и проверяем, превысил ли пользователь лимиты по каким-либо тегам. Затем сбрасываем notified в false, чтобы значение можно было повторно использовать cron-задачей checkLimits. Мы возвращаем булево значение, которое обработчик cron-задачи использует для каждого аккаунта пользователя и отправляет соответствующее уведомление в конце недели.

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;
};
Копировать

В приведённой выше функции мы снова перебираем все теги пользователя, по которым ещё не было уведомлений, и проверяем их использование. Если оно превысило установленный лимит, устанавливаем notified в true, чтобы этот тег не обрабатывался повторно до следующей недели. Мы возвращаем обработчику cron-задачи сообщение с указанием количества тегов, превысивших лимит, и хэша аккаунта пользователя.

Далее завершаем реализацию 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;
};
Копировать

Здесь мы используем Etherscan API для получения всех транзакций пользователя и обновления данных об использовании для каждого тега. Мы сортируем полученные транзакции в порядке убывания по времени и сканируем их начиная с транзакции latestHash, чтобы избежать повторного сканирования. В конце обновляем latestHash. Эта cron-задача выполняется чаще, чем checkLimits.

Вот несколько примеров вывода приведённых выше реализаций:

Фронтенд DApp

Функция тегирования

Сайт состоит из двух страниц. Первая — это лендинг, где пользователь может войти в систему, добавлять/удалять теги, просматривать распределение использования тегов, устанавливать лимиты и применять фильтры к тегам и транзакциям. Всё это реализовано с помощью стандартных возможностей React.js. Транзакции получаются через упомянутый ранее Etherscan API, а адреса сопоставляются с соответствующими тегами. Эта информация поступает из постоянного хранилища подключённых Snaps.

Логика большинства перечисленных функций написана в packages\site\src\pages\GetTableData.jsx. Интеграция snap с фронтендом достаточно сложна, но становится интуитивно понятной после завершения.

Методы snap открываются для фронтенда путём написания следующих функций в 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',
      },
    ],
  });
};
Копировать

Мы также используем npm-пакет react-chartjs-2 для отображения аналитики расходов пользователя.

Вот окончательный вид сайта:

Функция запроса суммы

Это уникальная функция, с помощью которой пользователь может отправить уведомление через XMTP другому пользователю с запросом на перевод ETH. Для этого оба пользователя должны иметь включённый snap. Функция доступна по адресу http://localhost:8000/request.

Для этого мы используем npm-пакет @xmtp/xmtp-js для создания WalletContext и XmtpContext в packages\site\src\contexts\WalletContext.tsx и packages\site\src\contexts\XmtpContext.tsx соответственно. Мы также создаём хуки и компоненты. Большая часть этого кода взята напрямую из примера в документации.

Мы создаём новую страницу packages\site\src\pages\request.tsx на сайте для размещения интерфейса этой функции. Сообщения отправляются с помощью функции sendMessage из хука useSendMessage.

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

const sendNewMessage = () => {
    const payload = {
      id: Date.now(),
      message: msgTxt,
    };
    sendMessage(JSON.stringify(payload));
    setMsgTxt('');
};
Копировать

Полученные сообщения отображаются с использованием состояния провайдера XmtpContext.

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

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

    return (
        <>
            <ConversationList
                convoMessages={convoMessages}
                setSelectedConvo={setSelectedConvo}
            />
        </>
    )
};
Копировать

После следования примеру из упомянутой выше документации мы в итоге получаем необходимую функциональность:

Заключение

На этом FoxTagger завершён — весь проект с нашей идеей, воплощённой в приложение MetaMask Snaps в связке с DApp.

Каковы следующие шаги, если бы вы продолжили реализацию?

Следующим шагом стало бы расширение функциональности тегирования за счёт добавления дополнительной аналитики и использования модели машинного обучения для прогнозирования и предложений по минимизации расходов пользователя. Затем можно было бы реализовать функцию разделения транзакций, позволяющую распределять комиссию между несколькими аккаунтами, дополняя функцию запроса суммы.

Кроме того, новая функция кастомного UI в MetaMask Snaps могла бы быть использована для обеспечения более приятного пользовательского опыта внутри snap. Наконец, snap можно расширить на другие сети помимо тестовой сети Goerli.

Расскажите немного о себе и своей команде?

От идеи до реализации наша команда совместно генерировала идеи, дорабатывала их и преодолевала трудности, чтобы создать FoxTagger Snaps.

Sachin Sahu сыграл ключевую роль в разработке фронтенда и функциональности dApp. Он не только помог интегрировать MetaMask Snaps с платформой, но и работал над демонстрацией и документацией, чтобы пользователи имели всю необходимую информацию для начала работы со Snap's dApp.

Экспертиза Siddhartha G в области проектирования и реализации фронтенда оказалась ключевой для развития платформы. Он создал интуитивно понятные компоненты и методы фронтенда для представления и изменения данных, а также помог интегрировать MetaMask Snaps с dApp.

Shreetesh M взял на себя реализацию бэкенда и обеспечил открытие всех функций для фронтенда. Он реализовал transaction insights и cron-задачи, что помогло команде бесшовно интегрировать бэкенд с фронтендом.

Вклад Noble Saji Mathews оказался неоценимым при разработке фреймворка для транзакций через on-chain-мессенджинг. Он также участвовал в процессе генерации идей для системы тегирования и запросов транзакций, что повышает удобство использования платформы.

Kranthi привнёс свежий взгляд, предложив идею системы тегирования и исследуя новые методы внедрения децентрализованной коммуникации. Его усилия привели к реализации функции «request», которая изменила способ взаимодействия пользователей со Snap's dApp.

Экспертиза Ansh Anand в области проектирования фронтенда и участие в процессе генерации идей сделали его незаменимым членом команды. Он работал над демонстрационным видео, которое продемонстрировало возможности платформы и помогло привлечь новых пользователей.

Какие возможности вы видите в MetaMask Snaps и то, что они открывают для пространства Web3?

При более чем 21 миллионе активных пользователей MetaMask довольно сложно реализовать функциональность, которая удовлетворяла бы потребности каждого. Именно здесь на помощь приходят MetaMask Snaps — сообщество разработчиков теперь имеет больше свободы для кастомизации и персонализации MetaMask под требования пользователей, что делает взаимодействие с различными dApps, построенными на разных блокчейнах или использующими разные протоколы, проще, чем когда-либо.

Преимущества MetaMask Snaps многочисленны: возможность отслеживать расходы с течением времени, анализировать траты, жертвовать небольшой процент от каждой транзакции на благотворительность, запрашивать оплату от клиентов, настраивать автоплатёж за подписку на Netflix и многое другое! Интеграция MetaMask с различными DeFi-протоколами, NFT-маркетплейсами и социальными сетями теперь стала проще простого благодаря MetaMask Snaps.

Но Snaps предназначены не только для сложных финансовых транзакций. Представьте, что вы идёте на обед с друзьями и платите криптовалютой — с MetaMask Snaps разделить счёт и запросить оплату от друзей так же просто, как одно нажатие! Вот в чём сила MetaMask Snaps!

По мере развития пространства Web3 MetaMask Snaps, несомненно, станет всё более важным инструментом как для разработчиков, так и для пользователей. С учётом потенциала метавселенной на горизонте сценарии использования Snaps поистине безграничны.

Какие советы вы хотели бы дать разработчикам, желающим попробовать MetaMask Snaps?

Мы бы посоветовали им сначала ознакомиться с существующими snaps на GitHub. Это поможет им лучше понять потенциал функций, предоставляемых MetaMask Snaps, хотя на бумаге эти функции могут казаться простыми.

Мы также рекомендуем использовать существующий шаблон monorepo в качестве отправной точки для реализации. У него много преимуществ: репозиторий хорошо структурирован, включает проверки линтинга и actions, разработчики могут реализовать как companion-веб-приложение, так и snap в одном пакете. Кроме того, репозиторий всегда соответствует официальной документации.

Наконец, мы рекомендуем публиковать свои вопросы и сомнения в каналах сообщества и расширять своё знакомство с MetaMask Snaps.

Разработка с MetaMask Snaps

Чтобы начать работу с MetaMask Snaps:

  1. Ознакомьтесь с документацией для разработчиков

  2. Установите MetaMask Flask

  3. Ознакомьтесь с руководством по MetaMask Snaps

  4. Оставайтесь на связи с нами в Twitter, GitHub discussions и Discord

Следите за нашей командой на следующем хакатоне рядом с вами! Удачного BUIDLing'а ⚒️

Отказ от ответственности: MetaMask Snaps, как правило, разрабатываются третьими сторонами, а не Consensys Software. Использование MetaMask Snaps, разработанных третьими сторонами, осуществляется по вашему собственному усмотрению и на ваш риск, и вы соглашаетесь с тем, что несёте единоличную ответственность за любые убытки или ущерб, возникшие в результате такой деятельности. Consensys не даёт никаких явных или подразумеваемых гарантий, устных или письменных, в отношении MetaMask Snaps, разработанных третьими сторонами, и отказывается от какой-либо ответственности за Snaps, разработанные третьими сторонами. Использование программного обеспечения, связанного с блокчейном, сопряжено с рисками, которые вы принимаете в полном объёме при использовании MetaMask Snaps.

Переведено ИИ. Может содержать ошибки. Пожалуйста, всегда проверяйте информацию.

Оцените перевод

Автор статьи:

  • MetaMask

    MetaMask, ранее известная как Consensys Software Inc, — крупнейшая в мире самостоятельная финансовая платформа, предоставляющая людям единое место для хранения, расходования, накопления и приумножения средств как в криптовалюте, так и в традиционных активах. Компания создаёт потребительскую платформу, объединяющую платежи, сбережения, инвестиции и цифровые активы в едином бесшовном опыте. Вырастая из самого широко используемого в мире self-custodial кошелька, MetaMask предоставляет пользователям прямой контроль над их деньгами и активами, охватывая около 190 стран. MetaMask сыграл основополагающую роль в развитии Ethereum с 2016 года. Сегодня MetaMask занимает центральное место в экономике блокчейна, создавая операционную систему для Open Money и предоставляя людям полный контроль над их финансовой жизнью.

    Читать все статьи