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

Интеграция Tenderly Transaction Simulations в Rabby Wallet

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

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

В этом руководстве вы узнаете, как интегрировать Tenderly Simulation API в расширение Chrome для Rabby Wallet. Сначала вы узнаете, как взаимодействовать с API и реализовать логику симуляции транзакций. Затем вы сгруппируете данные симуляции и выведете их пользователю. Таким образом, вы улучшите пользовательский опыт в этом примере Rabby Wallet, добавив новую функциональность.

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

Для этого руководства вам понадобятся: Начнём сборку! 👷‍♂️

Шаг 1. Клонируйте проект Rabby Wallet

Сначала форкните или клонируйте официальный репозиторий Rabby Wallet на GitHub. Для этого выполните:
example
git clone https://github.com/RabbyHub/Rabby.git
Далее перейдите в корень проекта и установите зависимости с помощью пакетного менеджера Yarn:
example
yarn install
Затем используйте следующую команду, чтобы запустить репозиторий локально:
example
yarn build:dev
Будет сгенерирована папка dist. Вы можете использовать её для загрузки расширения Chrome. Перейдите на chrome://extensions и включите Developer mode. Затем нажмите кнопку «Load unpacked», перейдите в директорию Rabby и выберите папку dist. Распакованное расширение появится на экране, и вы можете начать использовать Rabby Wallet. Теперь вы можете импортировать существующий кошелёк или создать новый.
Прежде чем начать, обязательно отключите продакшн-версию расширения Rabby Wallet для Chrome, если оно установлено.Пожалуйста, следуйте официальным рекомендациям от команды Rabby на случай, если возникнут проблемы с подключением.
Enabled unpacked version of the Rabby Wallet
Disabled production version of the Rabby Wallet

Шаг 2. Сгенерируйте токен доступа Tenderly

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

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

Шаг 3. Соберите логику симуляции

Далее мы рассмотрим следующие шаги:
  • Настройте безопасное хранилище для учётных данных Tenderly.
  • Создайте необходимый код для подключения к Tenderly Simulation API.
  • Используйте как приватные, так и публичные эндпоинты для взаимодействия с API.
  • Составьте правильный payload для этих эндпоинтов.

Работа с учётными данными Tenderly

Сначала вам нужно получить учётные данные Tenderly. Создайте файл /_raw/tenderly.json и обновите учётные данные Tenderly:
example.json
{
  "tenderlyAccount": "YOUR_ACCOUNT_NAME",
  "tenderlyProject": "YOUR_PROJECT_NAME",
  "tenderlyAccessToken": "YOUR_ACCESS_TOKEN"
}
Чтобы получить доступ к учётным данным Tenderly в вашем проекте, обновите build/webpack.common.config.js следующим образом:
example.js
const { tenderlyAccount, tenderlyProject, tenderlyAccessToken } = require('../_raw/tenderly.json');

// other code...

const config = {
  plugins: [
    // abbreviated
    new webpack.DefinePlugin({
      'process.env.version': JSON.stringify(`version: ${version}`),
      'process.env.release': JSON.stringify(version),
      // Tenderly Credentials
      'process.env.TENDERLY_ACCOUNT_SLUG': JSON.stringify(tenderlyAccount),
      'process.env.TENDERLY_PROJECT_SLUG': JSON.stringify(tenderlyProject),
      'process.env.TENDERLY_ACCESS_TOKEN': JSON.stringify(tenderlyAccessToken),
    }),
  ],
  resolve: {
    // abbreviated
  },
};
Вы можете найти реализацию здесь.

Реализация логики симуляции транзакций

Далее создайте файл src/ui/utils/tenderly.ts и реализуйте логику с помощью Tenderly Simulation API. Получите полный исходный код здесь.
example.tsx

export const simulateTransaction = async (tx: Tx): Promise<Record<string, any> | null> => {
  const TENDERLY_ACCOUNT_SLUG = process.env.TENDERLY_ACCOUNT_SLUG;
  const TENDERLY_PROJECT_SLUG = process.env.TENDERLY_PROJECT_SLUG;
  const TENDERLY_ACCESS_TOKEN = process.env.TENDERLY_ACCESS_TOKEN;

  if (!TENDERLY_ACCOUNT_SLUG || !TENDERLY_PROJECT_SLUG || !TENDERLY_ACCESS_TOKEN) {
    throw new Error('Tenderly credentials are not set up correctly.');
  }

  const txPayload: any = {
    ...tx,
    save: true,
    save_if_fails: true,
    simulation_type: 'full',
    source: 'rabby-wallet',
    network_id: tx.chainId.toString(),
    gas: parseInt(tx.gas as string, 16) || null,
    gas_price: parseInt(tx.gasPrice as string, 16) || null,
    value: parseInt(tx.value, 16) || null,
    input: tx.data,
  };

  try {
    const simulationResponse = await axios.post(
      `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_SLUG}/project/${TENDERLY_PROJECT_SLUG}/simulate`,
      txPayload,
      {
        headers: {
          'Content-Type': 'application/json',
          'X-Access-Key': TENDERLY_ACCESS_TOKEN as string,
        },
      },
    );

    // Make the simulation publicly accessible
    if (simulationResponse?.data?.simulation?.id) {
      await axios.post(
        `https://api.tenderly.co/api/v1/account/${TENDERLY_ACCOUNT_SLUG}/project/${TENDERLY_PROJECT_SLUG}/simulations/${simulationResponse.data.simulation.id}/share`,
        {},
        {
          headers: {
            'Content-Type': 'application/json',
            'X-Access-Key': TENDERLY_ACCESS_TOKEN as string,
          },
        },
      );
    }

    return simulationResponse?.data;
  } catch (e) {
    console.error({ e });
    return null;
  }
};
Здесь мы использовали два эндпоинта. Первый эндпоинт позволяет симулировать транзакцию с различными параметрами в любой из 104 сетей, поддерживаемых Tenderly. Перейдите к нашему справочнику Simulation API, чтобы увидеть все обязательные и необязательные поля для этого эндпоинта.
example
https://api.tenderly.co/api/v1/account/{accountId}/project/{projectId}/simulate
Второй эндпоинт позволяет сделать симулированную транзакцию публично доступной. Так вы можете поделиться ей с членами вашей команды или коллегами для дополнительных инсайтов или быстрого решения проблем.
example
https://api.tenderly.co/api/v1/account/{accountId}/project/{projectId}/simulations/{simulationID}/share
Последний шаг — вызвать функцию simulateTransaction внутри компонента SignTx.tx, поместив её внутри функции explainTx, которая вызывается перед подписанием транзакции. Подробности кода см. по этой ссылке.

Шаг 4. Отображение предпросмотра симулированной транзакции

Чтобы помочь пользователям понять, что произойдёт при отправке транзакций, вы можете отображать в UI данные симуляции в понятном виде. Например, когда пользователи кошелька симулируют транзакции, вы можете отображать, какие активы были переданы. В примере с Rabby Wallet мы создали три простых для понимания раздела:
  1. Tenderly Simulation Summary: думайте об этом как о табеле успеваемости транзакции. Он предоставляет базовую информацию о вашей транзакции с прямой ссылкой на подробный обзор симуляции в Tenderly Dashboard.
  2. Assets In & Assets Out: получите чёткий список всех активов (либо ERC20, либо NFT-токенов), с которыми ваш кошелёк собирается взаимодействовать.
  3. Tokens Transferred: просмотрите каждый перевод токена, происходящий в транзакции. Для дополнительной ясности сгруппируйте все переводы по адресу и выделите переводы нативной монеты.
Transaction preview for ERC20 token swap on Uniswap
Transaction preview for NFT token purchase on Joepegs

Tenderly Simulation Summary

Сначала создадим раздел Tenderly Simulation Summary. Здесь мы показываем статус симуляции, сеть, в которой выполнена транзакция, и значение газа. Чтобы построить этот раздел, вам нужно взаимодействовать с ответом Tenderly Simulation API. Начните с создания файла TenderlySimulationSummary.tsx. Вы можете найти полную реализацию кода ниже или в репозитории GitHub здесь.
example.tsx


const TenderlySimulationSummary = ({ simulation }: { simulation: Record<string, any> | null }) => {
  const isSuccess = !!simulation?.status;
  const simulationUrl = `https://dashboard.tenderly.co/${process.env.TENDERLY_ACCOUNT_SLUG}/${process.env.TENDERLY_PROJECT_SLUG}/simulator/${simulation?.id}`;
  const sharedSimulationUrl = `https://tdly.co/shared/simulation/${simulation?.id}`;

  return (
    <div className="tenderly-simulation-summary container">
      <p className="text-16 text-gray-title font-medium mb-12">Tenderly Simulation Summary</p>
      {!simulation && <div>Something went wrong.</div>}
      {simulation && (
        <div className="flex flex-col gap-8">
          <div className="flex items-center justify-between gap-2">
            <div>Status</div>
            <div className={isSuccess ? 'text-green' : 'text-red'}>
              {isSuccess ? 'Success' : 'Failed'}
            </div>
          </div>
          <div className="flex items-center justify-between gap-2">
            <div>Network</div>
            <div className="flex items-center gap-4">
              <div className="flex items-center rounded-full">
                <img
                  src={TenderlyNetwork[simulation.network_id]?.image}
                  className="h-16 rounded-full"
                  alt="Tenderly"
                />
              </div>
              <div>{TenderlyNetwork[simulation.network_id]?.name}</div>
            </div>
          </div>
          <div className="flex items-center justify-between gap-2">
            <div>Gas</div>
            <div>{simulation.gas}</div>
          </div>
          <Button
            className="tenderly-simulation-summary-button flex items-center justify-between gap-2 pb-0"
            href={simulationUrl}
            target="_blank"
            rel="noreferrer"
            size="large"
            block
          >
            <div>View details</div>
            <div className="flex items-center gap-2">
              <img src={TenderlySvg} className="h-32" alt="Tenderly" />
              <IconRightArrow />
            </div>
          </Button>
          <Button
            className="text-[#6E56CF] bg-white flex items-center justify-center gap-2 pb-0"
            href={sharedSimulationUrl}
            target="_blank"
            rel="noreferrer"
            type="link"
            size="middle"
            block
          >
            <div>Share simulation ✨</div>
          </Button>
        </div>
      )}
    </div>
  );
};

export default TenderlySimulationSummary;
Обратите внимание, как мы сгенерировали ссылки, ведущие пользователей в Tenderly Dashboard. Первая ссылка предназначена для аутентифицированных пользователей, а вторая — расшариваемая ссылка на симуляцию транзакции:
example.tsx
const simulationUrl = `https://dashboard.tenderly.co/${process.env.TENDERLY_ACCOUNT_SLUG}/${process.env.TENDERLY_PROJECT_SLUG}/simulator/${simulation?.id}`;
const sharedSimulationUrl = `https://tdly.co/shared/simulation/${simulation?.id}`;
Чтобы получить данные из симулированной транзакции, обратитесь к справочнику Tenderly Simulation API:

Tenderly Simulation API

Assets In & Assets Out

Второй раздел даёт представление об управлении и мониторинге потока активов в примере Rabby Wallet. Он показывает как входящие, так и исходящие активы, чтобы вы могли лучше видеть активность кошелька. Чтобы этого добиться, начните с создания файла TenderlyWalletChanges.tsx:
example.tsx


const TenderlyWalletChanges = ({
  account,
  assetChanges,
}: {
  account?: Account;
  assetChanges: any;
}) => {
  const { assetsInList, assetsOutList } = React.useMemo(() => {
    const assetsInList = assetChanges.filter(asset => asset.to === account?.address) || [];
    const assetsOutList = assetChanges.filter(asset => asset.from === account?.address) || [];

    return {
      assetsInList,
      assetsOutList,
    };
  }, [account, assetChanges]);

  return (
    <div className="TenderlyWalletChanges">
      <div className="TenderlyWalletChanges__Content">
        {/* Assets Out */}
        {assetsOutList?.length > 0 && (
          <>
            <p className="text-16 text-gray-title font-medium mb-4">Assets Out</p>
            <div className="flex flex-col gap-4 border border-gray-divider rounded-sm">
              {assetsOutList.map((token, index) => (
                <div
                  key={index}
                  className={clsx('flex gap-4 p-4', {
                    'border-t-[1px] border-gray-divider': index !== 0,
                  })}
                >
                  <div className="flex items-center justify-center p-4">
                    <img src={token.token_info.logo} className="w-60 rounded" alt="Tenderly" />
                  </div>
                  <div className="flex flex-col gap-4">
                    <div className="flex flex-col gap-1">
                      <div className="font-semibold">
                        {token.token_info.symbol.toUpperCase()}
                        {token.token_id && <span className="ml-2">#{Number(token.token_id)}</span>}
                      </div>
                      <div className="text-12 text-gray-content">{token.token_info.name}</div>
                    </div>
                    <div className="flex flex-wrap items-center gap-4">
                      <div className="font-semibold">{token.type}:</div>
                      <div className="text-red-forbidden">- {formatAmount(token.amount)}</div>
                      <div className="text-12 text-gray-content">
                        (≈ {formatUsdValue(new BigNumber(token.dollar_value).toFixed())})
                      </div>
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </>
        )}

        {/* Assets In */}
        {assetsInList?.length > 0 && (
          <>
            <p className="text-16 text-gray-title font-medium mt-4 mb-4">Assets In</p>
            <div className="flex flex-col gap-4 border border-gray-divider rounded-sm">
              {assetsInList.map((token, index) => (
                <div
                  key={index}
                  className={clsx('flex gap-4 p-4', {
                    'border-t-[1px] border-gray-divider': index !== 0,
                  })}
                >
                  <div className="flex items-center justify-center p-4">
                    <img src={token.token_info.logo} className="w-60 rounded" alt="Tenderly" />
                  </div>
                  <div className="flex flex-col gap-4">
                    <div className="flex flex-col gap-1">
                      <div className="font-semibold">
                        {token.token_info.symbol.toUpperCase()}
                        {token.token_id && <span className="ml-2">#{Number(token.token_id)}</span>}
                      </div>
                      <div className="text-12 text-gray-content">{token.token_info.name}</div>
                    </div>
                    <div className="flex flex-wrap items-center gap-4">
                      <div className="font-semibold">{token.type}:</div>
                      <div className="text-green">+ {formatAmount(token.amount)}</div>
                      <div className="text-12 text-gray-content">
                        (≈ {formatUsdValue(new BigNumber(token.dollar_value).toFixed())})
                      </div>
                    </div>
                  </div>
                </div>
              ))}
            </div>
          </>
        )}
      </div>
      <TenderlyCredits classname="mt-12" />
    </div>
  );
};

export default TenderlyWalletChanges;
Здесь мы отфильтровали массив изменений активов и создали два списка:
  1. Активы, поступающие в кошелёк
  2. Активы, уходящие из кошелька
Затем мы использовали вспомогательные функции для предпросмотра и форматирования данных. Однако вы также можете создать свой собственный UI-макет для отображения важной информации об активах. Узнайте больше об изменениях активов при симуляции здесь:

Изменения активов и балансов

Tokens Transferred

Раздел Token Transfers — более сложный компонент. Этот файл в первую очередь управляет макетом, поэтому сначала обсудим логику сортировки. Вы можете найти реализацию этого раздела здесь. Начните с создания файла TenderlyAssetChanges.tsx. Он содержит логику для представления двух вкладок: Tokens Transferred и Native Coins Transferred. Для этого вам нужно добавить логику сортировки, переключения между вкладками и группировки данных по адресу.
Tokens Transferred UI
Сначала сгруппируем данные и создадим два массива: переданные токены и переданные нативные монеты:
example.tsx
const assetChangesMappedByType = React.useMemo<
  Record<AssetChangesTabs, TransactionAssetChangeData[]>
>(
  () =>
    transactionAssetChanges.reduce(
      (
        acc: {
          nativeCoinsTransferred: TransactionAssetChangeData[];
          tokensTransferred: TransactionAssetChangeData[];
        },
        assetChange: TransactionAssetChangeData,
      ) => {
        const { type } = assetChange.tokenInfo;
        if (type === TokenType.NATIVE) {
          return {
            ...acc,
            [AssetChangesTabs.nativeCoins]: [...(acc?.nativeCoinsTransferred ?? []), assetChange],
          };
        }
        return {
          ...acc,
          [AssetChangesTabs.tokens]: [...(acc?.tokensTransferred ?? []), assetChange],
        };
      },
      { [AssetChangesTabs.nativeCoins]: [], [AssetChangesTabs.tokens]: [] },
    ),
  [assetChanges],
);
Далее напишем логику для обработки типов сортировки двумя способами: в хронологическом порядке и по адресу. Здесь нам нужно два компонента: ChronologicalContent.tsx и PerAddressContent.tsx. Реализацию можно найти в репозитории GitHub:

Группировка и предпросмотр всех компонентов

Наконец, соберём все компоненты вместе. Для этого вам нужно создать файл TenderlySimulationResult.tsx. Также вам нужно использовать хук useWallet(), предоставленный командой Rabby, для взаимодействия с подключённым адресом кошелька. Вот обзор кода:
example.tsx

const TenderlySimulationResult = ({ data }: { data: Record<string, any> | null }) => {
  const [account, setAccount] = React.useState<Account>();
  const wallet = useWallet();

  const init = async () => {
    const currentAccount = await wallet.syncGetCurrentAccount();

    if (currentAccount) {
      setAccount(currentAccount);
    }
  };

  React.useEffect(() => {
    init();
  }, []);

  return (
    <div className="tenderly-simulation-result mt-16">
      <ActionWrapper>
        {!data && <div className="bg-[#f5f6fa] sticky top-[64px] z-10">Something went wrong.</div>}
        {data && <TenderlySimulationSummary simulation={data.simulation} />}
      </ActionWrapper>
      {data?.transaction?.transaction_info.asset_changes && (
        <>
          <ActionWrapper>
            <TenderlyWalletChanges
              account={account}
              assetChanges={data.transaction.transaction_info.asset_changes}
            />
          </ActionWrapper>
          <ActionWrapper>
            <TenderlyAssetChanges
              assetChanges={data.transaction.transaction_info.asset_changes}
              contracts={data.contracts}
            />
          </ActionWrapper>
        </>
      )}
    </div>
  );
};

export default TenderlySimulationResult;

Что дальше?

Поздравляем! Вы добавили функцию предпросмотра транзакции в пример Rabby Wallet с помощью Tenderly Simulation API. 🎉 Чтобы углубиться в логику, ознакомьтесь с полным исходным кодом на GitHub. Кроме того, вы можете инициировать симуляции транзакций Tenderly тремя разными способами в зависимости от требований вашего проекта. Чтобы узнать о других способах интеграции симуляций транзакций в ваш кошелёк или dapp, ознакомьтесь со следующими ресурсами:

Simulation API

Симуляция транзакций

Simulation RPC