跳转到主要内容
本教程演示了如何使用 Tenderly Simulation API 向钱包添加”交易预览”选项。我们将向您展示如何将此选项添加到 MetaMask 钱包。 交易预览”功能允许用户在将交易提交到生产网络之前看到其确切结果。配备此功能的钱包可帮助用户在做出承诺之前更容易理解交易的经济影响。 模拟在区块链最新状态的一个 fork 中执行,处于一个安全、无风险的环境中。这有助于钱包用户避免发送恶意或易出错的交易。 您可以在 GitHub 上找到完整的演示仓库:

Tenderly Simulate Asset Changes Snap

Tenderly MetaMask Snap 演示
本教程仅用于演示目的,具体是为了说明如何为 MetaMask Snap 添加 交易预览选项。它不适用也不推荐用于任何其他 用途。

项目概览

本项目向您展示如何将交易模拟集成到 MetaMask 钱包中。我们将使用 MetaMask Snaps 您将学习如何连接到 Tenderly Simulation API、发送交易数据、获取模拟结果,并通过 MetaMask UI 向用户展示这些结果。这包括以美元表示的资产变化、原生资产余额变化、输出值、存储变化、事件日志以及调用追踪。 模拟后的交易随后还可以在 Tenderly 中打开,以进行进一步的调试和测试。 以下是我们将添加到 MetaMask Snap 的功能列表。类似的功能也可以添加到任何钱包中,帮助用户在发送交易时建立信心。
功能描述
模拟调试自动生成的链接,用于在 Tenderly 中检查和调试该模拟。
可公开分享的模拟能够启用/禁用模拟的公开分享。
以美元价值显示的资产变化人类可读的数据,包括自动转换为美元的资产变化。
原生资产余额变化能够跟踪合约执行期间原生资产余额的变化。
输出值查看合约调用的结果,显示合约将要返回的内容。
存储变化查看合约执行期间对存储所做的任何更改。
事件日志查看在合约执行过程中发出的事件。
调用追踪执行期间发生的所有函数调用的分解。

前置条件

在开始构建 MetaMask Snap 之前,请务必禁用”正式版”的 MetaMask,并 安装 MetaMask Flask 开发 插件
让我们开始构建!👷‍♂️

第 1 步:设置 MetaMask Snap 项目

安装 MetaMask 官方的 Create Snap CLI 并创建一个新项目。
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
    }
  }
}
当您运行应用并尝试连接钱包时,会弹出一个权限请求。 点击 ConnectApprove & 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.ts):管理 Tenderly API 的身份认证和访问。
  • 模拟simulation.ts):与 Tenderly API 交互以模拟交易并获取模拟结果的核心逻辑。
  • 格式化器formatter.ts):解析模拟结果并以对用户友好的方式在 MetaMask UI 中格式化它们的逻辑。

凭据访问

我们可以将所有负责请求、更新和获取 Tenderly API 凭据的方法存储在一个名为 credentials-access.ts 的文件中。在此查看源代码 最重要的方法包括:
  • fetchCredentials():此函数获取 Tenderly 项目的凭据。如果没有存储凭据,则触发 handleUpdateTenderlyCredentials 请求新的凭据。
  • handleUpdateTenderlyCredentials():此函数处理更新 Tenderly 项目凭据的流程。它通过调用 requestNewTenderlyCredentials 获取新凭据,然后使用带有 'update' 操作的 snap_manageState 方法保存它们。
  • requestNewTenderlyCredentials():此函数请求新的 Tenderly 凭据。它调用 requestCredentials 以接收原始凭据数据,并在返回带有 accountIdprojectIdaccessToken 的对象之前验证其正确性。
snap_manageState 是一个内置方法,允许 Snap 将最多 100 MB 的数据持久化到 磁盘。在这里了解更多。

模拟

接下来,我们需要创建将交易数据发送到 Tenderly Simulation API、进行模拟并获取结果的逻辑。 我们将使用 simulate API 端点。 端点: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 发送带有要模拟的交易数据的请求。
当交易被模拟后,我们可以通过调用 share API 端点使其公开可访问。使模拟可分享后,您可以复制该交易的链接并将其发送给任何人。共享的交易无需 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;
}

格式化器

一旦 API 返回模拟结果,我们需要解析这些数据,并以对用户友好的方式格式化,以便在 MetaMask UI 中显示。 我们可以将此逻辑存储在一个名为 formatter.ts 的文件中。在此查看源代码。 核心格式化方法包括:
  • formatResponse():此函数接收 Tenderly 项目模拟的原始数据和凭据,调用每个相关部分(如余额变化、输出值、资产变化等)的单个格式化函数,并返回一个包含所有格式化输出的面板。
  • formatBalanceDiff():此函数生成一个面板,显示交易中涉及的每个账户的余额变化。
  • formatOutputValue():如果存在输出值,此函数会创建一个面板来展示交易的输出值。如果可能,它还会解码输出。
  • formatAssetChanges():此函数格式化一个面板以显示任何资产变化,区分 ERC20、ERC721 及其他变化。
  • formatStorageChanges():此函数创建一个面板,列出交易期间对存储的任何更改。为了清晰起见,它对地址和嵌套数据结构进行了独特的格式化。
  • formatEventLogs():如果存在事件日志,此函数会为每笔交易展示它们。日志被格式化以增强可读性,并包含输入值。
  • formatCallTrace():此函数生成调用追踪的可视化层级结构,递归显示嵌套调用。
  • formatSimulationUrl():此函数返回一个指向 Tenderly Dashboard 上模拟完整详情的链接,以及一个单独的可分享链接。

第 4 步:在 MetaMask UI 中显示模拟数据

MetaMask Snap 单体仓库提供了一组预定义的 UI 元素和布局配置。我们将使用这些元素扩展现有的 MetaMask UI,以显示模拟结果。 当您安装我们的 MetaMask 示例后,我们将向您展示如何运行不同类型的模拟:
  • 使用预定义负载的成功交易模拟(成功的 ERC-20 转账和 NFT 转账)
  • 使用预定义负载的失败交易模拟
  • 使用任意负载的自定义交易模拟
Tenderly Snap UI
在本教程中,我们将向您展示如何使用预定义负载运行模拟,以及如何添加自定义负载并执行模拟。

使用预定义负载的成功模拟

要启动模拟,我们编写一个函数,从我们的钱包地址发送预定义的交易数据,并调用 eth_sendTransaction RPC 方法。
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 处理器,进而调用我们在 simulation.ts 中定义的自定义 simulate 函数。
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 响应以便输出。 模拟结果显示在 MetaMask UI 中。下方图片展示了一次将 1 USDC 成功转账给 demo.eth 的 ERC20 代币转账。该输出由 formatter.ts 文件中定义的函数生成。
ERC20 Transfer - send 1 USDC to demo.eth
例如,Asset Changes 区块显示了模拟产生的美元价值变化,它由 formatAssetChanges() 函数生成。 您还会获得一个指向该模拟交易的链接,可以在 Tenderly 中打开它,进一步使用 Debugger 检查它,或使用不同的值重新模拟。 此链接由 formatter.ts 文件中定义的 formatSimulationUrl() 函数生成。
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}`),
  ];
}

使用预定义负载的失败交易

在第二个示例中,我们将使用预定义负载模拟一笔失败的交易。 下方图片显示了一次将 1,000,000 USDC 转账给 demo.eth 的失败 ERC20 代币转账。
Preview of the failed transaction
我们还会获得一个指向 Tenderly Dashboard 的链接,用户可以在此检查堆栈追踪、事件、状态变化、gas 消耗等信息。这对理解交易失败的原因特别有用。 用户可以顺畅地继续调试失败的交易,并重新模拟以测试不同的解决方案。

使用自定义负载的模拟

要使用自定义负载运行模拟,您需要添加一个符合 Ethereum 交易规范的对象。
Custom payload UI
它可以包含以下字段:
  • fromDATA,20 字节 - 交易的发送方地址。
  • toDATA,20 字节 -(创建新合约时可选)交易的目标地址。
  • gasQUANTITY -(可选,默认:90000)为交易执行提供的 gas 的整数。它将返回未使用的 gas。
  • gasPriceQUANTITY -(可选,默认:待定)每单位已支付 gas 所用的 gasPrice 整数。
  • valueQUANTITY -(可选)随此交易发送的 value 整数。
  • dataDATA - 合约的编译代码,或已调用方法签名和已编码参数的哈希。
  • nonceQUANTITY -(可选)nonce 整数。这允许您覆盖使用相同 nonce 的自己的待处理交易。
所有数值都应为十六进制字符串,地址应为 Ethereum 地址(20 字节)。
Send a custom payload
输入自定义负载后,用户可以使用与之前相同的 sendTransaction 函数发起模拟。此操作会将自定义负载传递给 onTransaction 处理器,进而触发 simulate 函数运行模拟。

后续步骤

在本教程中,您学习了如何借助 Tenderly Simulation API 为 MetaMask Snap 添加交易预览选项。您可以在 GitHub 上找到本项目的源代码 Tenderly 上的交易模拟可以根据您的项目和需求以三种方式发起。通过以下帮助资源了解如何将模拟集成到您的 dapp 中:

Simulation UI

Simulation API

Simulation RPC