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:
Постоянное хранилище (Persistence storage)
Уведомления
Cron-задачи
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;
}
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;
}
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:
export const getStorage = async () => {
return await window.ethereum.request({
method: 'wallet_invokeSnap',
params: [
defaultSnapOrigin,
{
method: 'getPersistentStorage',
},
],
});
};
export const setStorage = async (data: Record<string, unknown> | void) => {
await window.ethereum.request({
method: 'wallet_invokeSnap',
params: [
defaultSnapOrigin,
{
method: 'setPersistentStorage',
params: data,
},
],
});
};
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:
Ознакомьтесь с документацией для разработчиков
Установите MetaMask Flask
Ознакомьтесь с руководством по MetaMask Snaps
Оставайтесь на связи с нами в Twitter, GitHub discussions и Discord
Следите за нашей командой на следующем хакатоне рядом с вами! Удачного BUIDLing'а ⚒️
Отказ от ответственности: MetaMask Snaps, как правило, разрабатываются третьими сторонами, а не Consensys Software. Использование MetaMask Snaps, разработанных третьими сторонами, осуществляется по вашему собственному усмотрению и на ваш риск, и вы соглашаетесь с тем, что несёте единоличную ответственность за любые убытки или ущерб, возникшие в результате такой деятельности. Consensys не даёт никаких явных или подразумеваемых гарантий, устных или письменных, в отношении MetaMask Snaps, разработанных третьими сторонами, и отказывается от какой-либо ответственности за Snaps, разработанные третьими сторонами. Использование программного обеспечения, связанного с блокчейном, сопряжено с рисками, которые вы принимаете в полном объёме при использовании MetaMask Snaps.