Перейти к основному содержанию
В этом руководстве показано, как добавить опцию «предпросмотр транзакции» в кошелёк с помощью Tenderly Simulation API. Мы покажем, как добавить эту опцию в кошелёк MetaMask. Функция «предпросмотр транзакции» позволяет пользователям увидеть точный результат их транзакций до отправки в продакшн-сеть. Кошельки, оснащённые этой функцией, упрощают пользователям понимание финансовых последствий их транзакций перед принятием обязательств. Симуляции выполняются на форке актуального состояния блокчейна в безопасной среде без риска. Это помогает пользователям кошельков предотвращать отправку вредоносных или ошибочных транзакций. Полный демонстрационный репозиторий можно найти на GitHub:

Tenderly Simulate Asset Changes Snap

Демонстрация Tenderly MetaMask Snap
Это руководство предназначено только для демонстрационных целей, а именно для иллюстрации того, как добавить опцию предпросмотра транзакции в MetaMask Snap. Оно не предназначено и не рекомендуется для любого другого использования.

Обзор проекта

Этот проект показывает, как интегрировать симуляции транзакций в кошелёк MetaMask. Мы будем работать с MetaMask Snaps. Вы узнаете, как подключаться к Tenderly Simulation API, отправлять данные транзакции, получать результаты симуляции и отображать их пользователю через UI MetaMask. Сюда входят изменения активов в долларах, изменения баланса нативных активов, выходное значение, изменения хранилища, логи событий и трейсы вызовов. Симулированные транзакции затем можно открыть в Tenderly для дальнейшей отладки и тестирования. Ниже приведён список функций, которые мы будем добавлять в MetaMask Snap. Похожие функции можно также добавить в любой кошелёк, чтобы помочь пользователям обрести уверенность при отправке транзакций.
ФункцияОписание
Отладка симуляцииАвтоматически сгенерированная ссылка для инспекции и отладки симуляции в Tenderly.
Публично доступные симуляцииВозможность включать/отключать публичный доступ к симуляциям.
Изменения активов в долларовом эквивалентеДанные в понятном формате, включая изменения активов, автоматически преобразованные в доллары.
Изменения баланса нативных активовВозможность отслеживать изменения баланса нативных активов во время выполнения контракта.
Выходное значениеУвидеть результат вызова контракта, отображающий то, что контракт должен вернуть.
Изменения хранилищаУвидеть любые изменения хранилища контракта во время выполнения.
Логи событийУвидеть события, которые были сгенерированы во время выполнения контракта.
Трейсы вызововРазбивка всех вызовов функций, произошедших во время выполнения.

Предварительные требования

  • Установленное расширение браузера MetaMask Flask
  • Node.js (версии 16 или новее)
  • Yarn (версии 3)
Перед началом сборки MetaMask Snap обязательно отключите «реальную» версию MetaMask и установите плагин MetaMask Flask Development.
Начнём сборку! 👷‍♂️

Шаг 1. Настройка проекта MetaMask Snap

Установите официальный Create Snap CLI от MetaMask и создайте новый проект.
example
yarn create @metamask/snap project-name

OR

npm create @metamask/snap project-name
Далее нам нужно запустить проект Snap и обслуживать фронтенд на https://localhost:8000. Из корня только что созданного проекта установите зависимости проекта с помощью Yarn.
example
yarn
Запустите сервер разработки.
example
yarn start

Установка правильных разрешений

После установки Snap найдите файл /packages/snap/snap.manifest.json и установите разрешения, как показано ниже. Узнайте о разрешениях MetaMask здесь.
example.json
{
  // ...
  "initialPermissions": {
    "snap_dialog": {},
    "endowment:rpc": {
      "dapps": true,
      "snaps": false
    },
    "endowment:transaction-insight": {
      "allowTransactionOrigin": true
    },
    "endowment:network-access": {},
    "snap_manageState": {},
    "endowment:ethereum-provider": {
      "dapps": true,
      "snaps": true
    }
  }
}
Когда вы запустите приложение и попытаетесь подключить кошелёк, появится запрос на разрешение. Нажмите кнопки Connect и Approve & install, чтобы продолжить.
Install MetaMask Snap Permissions

Шаг 2. Генерация токенов доступа Tenderly API

Опция предпросмотра транзакции в MetaMask Snap работает на базе Tenderly Simulation API. Прежде чем начать использовать API, вам нужно сгенерировать токены доступа. Выполните следующие шаги:
  1. Войдите в Tenderly или создайте бесплатный аккаунт здесь.
  2. Перейдите на страницу Authorization и нажмите кнопку Generate Access Token.
Если вам нужна помощь с аутентификацией API, следуйте этому руководству:

Как сгенерировать токены доступа API

Generate Access Token from Authorization page

Шаг 3. Написание логики для Snap

Основная логика, обеспечивающая работу Snap, хранится в трёх файлах:
  • Credentials Access (credentials-access.ts): управляет аутентификацией и доступом к Tenderly API.
  • Simulation (simulation.ts): основная логика взаимодействия с Tenderly API для симуляции транзакций и получения результатов симуляции.
  • Formatter (formatter.ts): логика для парсинга результатов симуляции и форматирования их в удобном виде в UI MetaMask.

Credentials Access

Все методы, отвечающие за запрос, обновление и получение учётных данных Tenderly API, можно хранить в файле credentials-access.ts. См. исходный код здесь. Наиболее важные методы:
  • fetchCredentials(): эта функция получает учётные данные проекта Tenderly. Если учётные данные не сохранены, она запускает handleUpdateTenderlyCredentials для запроса новых.
  • handleUpdateTenderlyCredentials(): эта функция обрабатывает процесс обновления учётных данных проекта Tenderly. Она получает новые учётные данные, вызывая requestNewTenderlyCredentials, а затем сохраняет их с помощью метода snap_manageState с операцией 'update'.
  • requestNewTenderlyCredentials(): эта функция запрашивает новые учётные данные Tenderly. Она вызывает requestCredentials, чтобы получить сырые данные учётных записей и проверить их правильность перед возвратом объекта с accountId, projectId и accessToken.
snap_manageState — встроенный метод, позволяющий Snap сохранять до 100 МБ данных на диск. Узнайте больше здесь.

Simulation

Далее нам нужно создать логику, которая отправляет данные транзакции в Tenderly Simulation API, симулирует их и получает результаты. Для этого мы будем использовать эндпоинт API simulate. Эндпоинт: https://api.tenderly.co/api/v1/account/{accountId}/project/{projectId}/simulate Чтобы всё было организованно, поместим логику симуляции в файл simulation.ts. См. исходный код здесь. Чтобы отправить данные транзакции в Tenderly Simulation API, нам нужно создать два ключевых метода:
  • simulate(): это основная функция, которая обрабатывает симуляцию транзакции. Она получает учётные данные доступа к API, отправляет данные транзакции в Tenderly Simulation API и обрабатывает любые ошибки, возвращаемые API.
example.tsx
export async function simulate(
  transaction: { [key: string]: Json },
  transactionOrigin: string,
): Promise<Panel> {
  const credentials = await fetchCredentials(transactionOrigin);

  if (!credentials) {
    return panel([text('🚨 Tenderly access token updated. Please try again.')]);
  }

  const simulationResponse = await submitSimulation(transaction, credentials);
  const err = catchError(simulationResponse, credentials);

  return err || formatResponse(simulationResponse, credentials);
}
  • submitSimulation(): эта функция отправляет запрос в API вместе с данными транзакции для симуляции.
Когда транзакция симулируется, мы можем сделать её публично доступной, вызвав API-эндпоинт share. Возможность поделиться симуляцией позволяет копировать ссылку на транзакцию и отправлять её кому угодно. Расшаренные транзакции можно просматривать без учётной записи Tenderly.
example.tsx
async function submitSimulation(
  transaction: { [key: string]: Json },
  credentials: TenderlyCredentials,
) {
  const chainId = await ethereum.request({ method: 'eth_chainId' });
  const response = await fetch(
    `https://api.tenderly.co/api/v1/account/${credentials.accountId}/project/${credentials.projectId}/simulate`,
    {
      method: 'POST',
      body: JSON.stringify({
        from: transaction.from,
        to: transaction.to,
        input: transaction.data,
        gas: hex2int(transaction.gas),
        value: hex2int(transaction.value),
        network_id: hex2int(chainId as string),
        save: true,
        save_if_fails: true,
        simulation_type: 'full',
        generate_access_list: false,
        source: 'tenderly-metamask-snap',
      }),
      headers: {
        'Content-Type': 'application/json',
        'X-Access-Key': credentials.accessToken,
      },
    },
  );

  const parsedResponse = await response.json();

  // Make the simulation publicly accessible
  if (parsedResponse?.simulation?.id) {
    await fetch(
      `https://api.tenderly.co/api/v1/account/${credentials.accountId}/project/${credentials.projectId}/simulations/${parsedResponse.simulation.id}/share`,
      {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          'X-Access-Key': credentials.accessToken,
        },
      },
    );
  }

  return parsedResponse;
}

Formatter

После того как API вернёт результаты симуляции, нам нужно распарсить эти данные и отформатировать их в удобном для пользователя виде для отображения в UI MetaMask. Эту логику можно хранить в файле formatter.ts. См. исходный код здесь. Основные методы форматирования:
  • formatResponse(): эта функция получает сырые данные и учётные данные симуляции проекта Tenderly, вызывает индивидуальные функции форматирования для каждого релевантного раздела (например, изменения балансов, выходное значение, изменения активов и т. д.) и возвращает панель со всеми отформатированными выводами.
  • formatBalanceDiff(): эта функция генерирует панель, показывающую изменения балансов для каждого аккаунта, участвующего в транзакции.
  • formatOutputValue(): эта функция создаёт панель, представляющую выходные значения транзакции, если таковые имеются. Также она декодирует вывод, если это возможно.
  • formatAssetChanges(): эта функция форматирует панель для отображения любых изменений активов, различая изменения ERC20, ERC721 и другие.
  • formatStorageChanges(): эта функция создаёт панель, в которой перечислены любые изменения хранилища, произошедшие во время транзакции. Она уникально форматирует адреса и вложенные структуры данных для наглядности.
  • formatEventLogs(): эта функция представляет логи событий, если они существуют, для каждой транзакции. Логи отформатированы для удобства чтения и включают входные значения.
  • formatCallTrace(): эта функция создаёт отформатированную визуальную иерархию трейсов вызовов, рекурсивно показывая вложенные вызовы.
  • formatSimulationUrl(): эта функция возвращает ссылку на полные детали симуляции в Tenderly Dashboard и отдельную ссылку для расшаривания.

Шаг 4. Отображение данных симуляции в UI MetaMask

Монорепозиторий MetaMask Snap предоставляет набор предопределённых UI-элементов и конфигураций макета. Мы будем использовать эти элементы для расширения существующего UI MetaMask и отображения результатов симуляции. Когда вы устанавливаете наш пример для MetaMask, мы показываем, как запускать разные типы симуляций:
  • Симуляция успешной транзакции с предопределённым payload (успешный трансфер ERC-20 и трансфер NFT)
  • Симуляция неудачной транзакции с предопределённым payload
  • Симуляция пользовательской транзакции с любым payload
Tenderly Snap UI
Для целей этого руководства мы покажем, как запускать симуляции с предопределённым payload, а также как добавить свой собственный payload и выполнить симуляцию.

Успешная симуляция с предопределённым payload

Чтобы инициировать симуляцию, мы напишем функцию, которая будет отправлять предопределённые данные транзакции с нашего адреса кошелька и вызывать RPC-метод eth_sendTransaction.
example.tsx
// packages/site/src/utils/snap.ts

export const sendTransaction = async (data: any): Promise<any> => {
  try {
    const [from] = (await window.ethereum.request({
      method: 'eth_requestAccounts',
    })) as string[];

    if (!from) {
      return Promise.reject(Error('Failed to get an account'));
    }

    // https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction
    return window.ethereum.request({
      method: 'eth_sendTransaction',
      params: [
        {
          ...data,
          from,
        },
      ],
    });
  } catch (e) {
    console.error(e);
    return e;
  }
};
Вызов метода eth_sendTransaction запустит обработчик onTransaction, который вызовет нашу пользовательскую функцию simulate, определённую в simulation.ts.
example.tsx
// packages/snap/src/index.ts

export const onTransaction: OnTransactionHandler = async ({ transaction, transactionOrigin }) => {
  if (!isObject(transaction) || !hasProperty(transaction, 'to')) {
    return {
      content: {
        value: 'Unknown transaction type',
        type: NodeType.Text,
      },
    };
  }

  const simulationResponse = await simulate(transaction, transactionOrigin || '');

  return {
    content: {
      children: simulationResponse.children,
      type: NodeType.Panel,
    },
  };
};
В обработчике onTransaction функция simulate вызывается с транзакцией и её источником в качестве аргументов. Функция simulate отвечает за получение учётных данных доступа, отправку данных транзакции в Tenderly API, обработку любых ошибок и форматирование ответа API для вывода. Результаты симуляции отображаются в UI MetaMask. На изображениях ниже показан успешный трансфер токена ERC20 в 1 USDC на demo.eth. Вывод генерируется функциями, определёнными в файле formatter.ts.
ERC20 Transfer - send 1 USDC to demo.eth
Например, блок Asset Changes, показывающий изменения в долларовом эквиваленте в результате симуляции, генерируется функцией formatAssetChanges(). Вы также получите ссылку на симулированную транзакцию, которую можно открыть в Tenderly и исследовать её далее в Debugger либо ресимулировать с другими значениями. Эта ссылка генерируется функцией formatSimulationUrl(), определённой в файле formatter.ts.
example.ts
export function formatSimulationUrl(data: any, credentials: TenderlyCredentials): Component[] {
  const simulationUrl = `https://dashboard.tenderly.co/${credentials.accountId}/${credentials.projectId}/simulator/${data.simulation?.id}`;
  const sharedSimulationUrl = `https://tdly.co/shared/simulation/${data.simulation?.id}`;

  return [
    heading('Tenderly Dashboard:'),
    text('See full simulation details in Tenderly.'),
    text(`**Status:** ${data.transaction?.status ? 'Success ✅' : 'Failed ❌'}`),
    copyable(`${simulationUrl}`),
    text('Share simulation details with others! 🤗'),
    copyable(`${sharedSimulationUrl}`),
  ];
}

Неудачная транзакция с предопределённым payload

Во втором примере мы симулируем неудачную транзакцию с предопределённым payload. На изображении ниже показан неудачный трансфер токена ERC20 в 1 000 000 USDC на demo.eth.
Preview of the failed transaction
Мы также получаем ссылку на Tenderly Dashboard, где пользователь может изучить трассировку стека, события, изменения состояния, потребление газа и т. д. Это особенно полезно для понимания причин сбоя транзакции. Пользователь может продолжить отладку неудачной транзакции и ресимулировать её для проверки различных решений.

Симуляция с пользовательским payload

Чтобы запустить симуляцию с пользовательским payload, вам нужно добавить объект, соответствующий спецификации транзакций Ethereum.
Custom payload UI
Он может содержать следующие поля:
  • from: DATA, 20 байт — адрес, с которого отправляется транзакция.
  • to: DATA, 20 байт — (опционально при создании нового контракта) адрес, на который направляется транзакция.
  • gas: QUANTITY — (опционально, по умолчанию: 90000) целое число газа, предоставленного для выполнения транзакции. Неиспользованный газ будет возвращён.
  • gasPrice: QUANTITY — (опционально, по умолчанию: To-Be-Determined) целое число gasPrice, используемого для каждого оплаченного газа.
  • value: QUANTITY — (опционально) целое число значения, отправляемого с этой транзакцией.
  • data: DATA — скомпилированный код контракта ИЛИ хэш вызываемой сигнатуры метода и закодированные параметры.
  • nonce: QUANTITY — (опционально) целое число nonce. Это позволяет перезаписывать ваши собственные ожидающие транзакции, использующие тот же nonce.
Все числовые значения должны быть шестнадцатеричными строками, а адреса — адресами Ethereum (20 байт).
Send a custom payload
После ввода пользовательского payload пользователи могут инициировать симуляцию с помощью той же функции sendTransaction, что и раньше. Это передаёт пользовательский payload в обработчик onTransaction, который затем запускает функцию simulate для выполнения симуляции.

Следующие шаги

В этом руководстве вы узнали, как добавить опцию предпросмотра транзакции в MetaMask Snap на базе Tenderly Simulation API. Исходный код этого проекта на GitHub. Симуляция транзакций на Tenderly может быть инициирована тремя способами, в зависимости от вашего проекта и потребностей. Узнайте, как интегрировать симуляции в свои dapp’ы, с помощью этих справочных ресурсов:

Simulation UI

Simulation API

Simulation RPC