跳转到主要内容
本教程展示了如何借助 Tenderly Simulation API 将”交易预览”功能集成到 Rabby Wallet 示例中。 交易预览使钱包用户可以模拟交易执行,并在不将交易发送到链上的情况下预览确切结果。这样,用户可以更好地了解交易的经济影响,并有信心地发送它们。此外,您还可以帮助保护用户的资金、消除不必要的 gas 费用,并改善产品的整体用户体验和安全性。 以下是此开源示例的 GitHub 仓库:

将 Tenderly 交易模拟集成到 Rabby Wallet 中

Tenderly Rabby 交易预览演示
本教程仅用于说明目的,展示如何将交易预览集成到 Rabby Wallet 中。它不适用也不推荐用于任何其他用途。

项目概览

在本教程中,您将学习如何将 Tenderly Simulation API 集成到 Rabby Wallet Chrome 扩展中。首先,您将学习如何与 API 交互并实现交易模拟逻辑。然后,您将对模拟数据进行分组并向用户展示。通过这种方式,您将通过添加新功能来改善此 Rabby Wallet 示例中的用户体验。

前置条件

对于本教程,您需要: 让我们开始构建!👷‍♂️

第 1 步:克隆 Rabby Wallet 项目

首先,fork 或克隆官方的 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 交互。
  • 为这些端点构建合适的负载。

处理 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;
  }
};
在这里,我们使用了两个端点。 第一个端点允许您在 Tenderly 支持的104 个网络中的任意一个上以不同参数模拟交易。前往我们的 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
最后一步是在 SignTx.tx 组件中调用 simulateTransaction 函数,将其放入在交易签名之前被调用的 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 部分。在这里,我们展示模拟状态、交易执行的网络和 gas 值。 要构建此部分,您需要与 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 TransferredNative 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.tsxPerAddressContent.tsx 您可以在 GitHub 仓库中找到实现:

组合和预览所有组件

最后,让我们把所有组件放在一起。为此,您需要创建一个 TenderlySimulationResult.tsx 文件。您还需要使用 Rabby 团队提供的 useWallet() hook 与已连接的钱包地址交互。 以下是代码概览:
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;

后续内容

恭喜!您已使用 Tenderly Simulation API 将交易预览功能添加到 Rabby Wallet 示例中。🎉 要深入了解逻辑,请查看 GitHub 上的完整源代码 此外,您可以根据项目需求使用三种不同的方法发起 Tenderly 交易模拟。要了解将交易模拟集成到您的钱包或 dapp 的其他方式,请查看以下资源:

Simulation API

模拟交易

Simulation RPC