如何使用 Ethers.js 发送 EIP-7702 交易

本文介绍了如何使用 Ethers.js 实现 EIP-7702 交易,EIP-7702 允许 EOA 临时具有智能合约功能,从而实现批量交易、Gas 赞助和自定义逻辑等功能。文章提供了详细的步骤,包括环境设置、核心概念讲解、代码示例和问题排查,帮助开发者将 EIP-7702 集成到他们的 dApp 中,并介绍了如何撤销授权。

概述

以太坊的 Pectra 升级 引入了 EIP-7702,使外部所有账户 (EOA) 能够暂时采用智能合约功能。这连接了传统钱包和智能合约账户,解锁了批量交易、gas 赞助和自定义逻辑等功能,而无需更改你的地址。

本指南提供了使用 Ethers.js 实现 EIP-7702 交易的分步过程。你将学习发送批量交易、管理赞助交易、撤销授权,以及解决诸如 nonce 管理和授权签名等挑战,确保无缝集成到你的 DApp 中。

你将做什么

  • 使用 Ethers.js 发送 EIP-7702 交易(0x04 类型)
  • 使用 Ethers.js 撤销现有委托
  • 学习委托和授权如何工作
  • 学习如何处理诸如 nonce 管理和授权签名等常见问题

你需要什么

  • 具有以太坊 Sepolia 端点的 QuickNode 账户
  • EIP-7702 的基本知识
  • 已安装 Node.js (v20+ 推荐) 和 TypeScript
  • 已安装 Ethers.js
  • 用于测试的 Web3 兼容钱包(例如,MetaMask)- 在本指南中,我们将使用两个钱包:
    • 首个签名者: 将发送交易的 EOA
    • 赞助者签名者: 将赞助交易 gas 费用的 EOA
依赖 版本
Node.js >v20
Ethers.js >6.14.3

EIP-7702 核心概念

在深入研究实现之前,了解 EIP-7702 独特和强大的原因至关重要。与之前的账户抽象方法不同,EIP-7702 允许你现有的 EOA 获得智能合约功能,而无需更改其地址或进行复杂的迁移。

有关 EIP-7702 的更多详细信息

在本节中,我们将介绍 EIP-7702 的一些核心概念。有关更多详细信息,请参阅 EIP-7702 规范EIP-7702 实现指南

委托机制

当你将 EOA 委托给智能合约时,你实际上是在告诉以太坊网络,每当有人与你的 EOA 地址交互时,都要执行该合约的代码。可以将其视为使用新功能临时“升级”你的钱包,同时保留你熟悉的地址和私钥。

委托过程涉及创建指向你选择的实现合约的授权签名。一旦此授权在链上处理完毕,你的 EOA 的代码槽将包含一个特殊的委托指示符,该指示符将执行重定向到目标合约。

// 委托指示符格式:0xef0100 + contract_address
// 示例:0xef01001234567890123456789012345678901234567890

此委托将保持活动状态,直到你明确更改或撤销它,使其成为持久增强,而不是每个交易的功能。

交易构建

在标准以太坊交易中,调用智能合约函数涉及将 to 字段设置为合约的地址,并提供编码的函数调用数据。使用 EIP-7702,你可以将 to 字段设置为外部所有账户 (EOA) 本身,并包含指向实现合约函数的数据,以及签名的授权消息。

由于 EOA 在 EIP-7702 交易期间充当智能合约,因此可以预期此行为,但是对于习惯于传统模型的开发人员来说,这非常令人困惑。理解这种根本区别对于成功实现 EIP-7702 至关重要。

授权 Nonce

用户 (EOA) 签署一条授权消息,其中包括链 IDnonce委托地址和签名组件(y_parityrs)。构建此消息时的一个关键细节是如何正确设置 nonce。

在非赞助交易的情况下,即同一账户既发送交易又授权委托,你必须在签名的授权消息中使用该账户的当前 nonce 加一(current_nonce + 1)。

为什么需要这样做?

正如 EIP-7702 规范中定义的那样:

授权列表在交易的执行部分开始之前处理,但在发送者的 nonce 递增之后处理。

因此,当 EVM 处理授权列表时,它已经递增了发送者的 nonce。在验证期间,它会检查授权的链上 nonce 是否与授权中的 nonce 匹配。

设置你的开发环境

正如我们在上一节中介绍的关键概念一样,让我们开始设置你的开发环境。

前提条件

QuickNode 以太坊 Sepolia 端点

首先,你需要一个具有以太坊 Sepolia 端点的 QuickNode 账户。如果你没有,可以在此处创建一个。然后,创建一个新的以太坊 Sepolia 端点,并将提供的 HTTPS URL 放在手边以供以后使用。

以太坊 Sepolia 水龙头

要测试 EIP-7702 交易,你需要在钱包中拥有一些 Sepolia ETH 和 USDC(或其他 ERC-20 代币)。

对于 Sepolia ETH,你可以使用 QuickNode 多链水龙头:

  • 转到 QuickNode 多链水龙头
  • 连接或粘贴你的钱包地址,然后选择以太坊 Sepolia 测试网
  • 你还可以发推文获得奖励!

注意: 你需要在以太坊主网上至少拥有 0.001 ETH 才能使用 EVM 水龙头。

对于 USDC,你可以使用 Circle 的 Sepolia USDC 水龙头:

依赖项

首先,如果你没有全局安装 TypeScripttsx,则可以使用以下命令安装它们:

npm install -g typescript tsx

tsx 是一个 TypeScript 执行引擎,允许你直接运行 TypeScript 文件,而无需先编译它们。它是一个出色的开发和测试工具。

然后,创建一个新的 Node.js 项目并安装所需的软件包。我们将使用 Ethers.js 6.14.3 或更高版本,其中包括完整的 EIP-7702 支持。

mkdir eip7702-example && cd eip7702-example
npm init -y
npm install ethers dotenv

环境变量

在项目根目录中创建一个 .env 文件,以存储你的 QuickNode 端点 URL、私钥和其他配置详细信息。这会将敏感信息保留在你的源代码之外。

## QuickNode Sepolia RPC 端点
QUICKNODE_URL="YOUR_ENDPOINT_URL"

## 用于测试的私钥
FIRST_PRIVATE_KEY="YOUR_PRIVATE_KEY_FOR_FIRST_WALLET"
SPONSOR_PRIVATE_KEY="YOUR_PRIVATE_KEY_FOR_SPONSOR_WALLET"

## 我们在 Sepolia 上的示例委托合约地址
DELEGATION_CONTRACT_ADDRESS = "0x69e2C6013Bd8adFd9a54D7E0528b740bac4Eb87C"

## Sepolia 上的 USDC 地址
USDC_ADDRESS = "0x1c7d4b196cb0c7b01d743fbc6116a902379c7238"

将占位符值替换为你的实际 QuickNode 端点 URL 和钱包私钥。委托合约地址属于 Sepolia 上的一个示例实现,我们在 EIP-7702 实现指南 中对此进行了介绍。如果你已部署了一个合约,请随意使用你自己的合约地址。

合约 ABI

要与委托合约交互,我们需要它的 ABI(应用程序二进制接口)。要获取合约的完整 ABI,请转到 Etherscan Sepolia 合约,复制 ABI 部分并将其粘贴到一个类似于以下示例的新文件中。

如果你部署自己的合约,请确保将 ABI 替换为你的合约的 ABI。

创建一个名为 contract.ts 的新文件并添加以下代码:

contract.ts

export const contractABI = [\
  "function execute((address,uint256,bytes)[] calls) external payable",\
  "function execute((address,uint256,bytes)[] calls, bytes signature) external payable",\
  "function nonce() external view returns (uint256)"\
];

了解委托合约

示例委托合约有两个 execute 函数,一个用于非赞助(直接)交易,一个用于赞助交易。这两个函数都适用于批量执行。它还包括一个 nonce 函数,用于跟踪授权的当前 nonce。

签名验证的原因是确保 EOA 授权交易,尤其是在其他帐户支付 gas 费用的赞助交易中。如果没有这样的检查,任何人都可以代表 EOA 执行交易,而未经其同意。

委托合约的部分视图

struct Call {
    address to;
    uint256 value;
    bytes data;
}

// 直接执行 (msg.sender == address(this))
function execute(Call[] calldata calls) external payable;

// 赞助执行 (需要签名验证)
function execute(Call[] calldata calls, bytes calldata signature) external payable;

// 用于签名验证的 Nonce
function nonce() external view returns (uint256);

使用 Ethers.js 发送 EIP-7702 交易

现在我们已经设置了环境并了解了核心概念,让我们使用 Ethers.js 实现 EIP-7702 交易。我们将介绍以下步骤:

  1. 初始化 Ethers.js 并检查委托状态:使用你的 QuickNode 端点设置 Ethers.js,并检查你的 EOA 是否有现有委托。
  2. 为 EOA 创建授权:使用 nonce + 1 规则为 EOA 创建授权,该授权将在后续交易中使用。
  3. 发送非赞助的 EIP-7702 交易:发送 EOA 支付自己的 gas 费用的交易,为授权使用 nonce + 1 规则。在此交易中,EOA 将向两个不同的地址发送 0.002 和 0.001 ETH。
  4. 发送赞助的 EIP-7702 交易:发送赞助商支付 gas 费用的交易。由于我们在上一步中设置了授权,因此我们不再需要同一 EOA 的新授权。在此交易中,赞助商将执行 EOA 将 0.1 USDC 和 0.001 ETH 转移到接收者地址的交易。
  5. 撤销委托:可以选择撤销委托以将你的 EOA 恢复到其原始状态。
  6. 运行完整的工作流程:将所有步骤合并到一个主函数中以执行交易。

步骤 1:初始化 Ethers.js 并检查委托状态

首先,让我们设置 Ethers.js 以连接到你的 QuickNode 端点,并验证你的 EOA 是否具有现有委托。

此代码使用你的 QuickNode 端点初始化 Ethers.js,并通过检查其代码(EIP-7702 委托以 0xef0100 开头)来检查你的 EOA 是否具有活动的 EIP-7702 委托。

创建一个名为 index.ts 的新文件并添加以下代码:

import dotenv from "dotenv";
import { ethers } from "ethers";
import { contractABI } from "./contract";

dotenv.config();

// 用于可重用性的全局变量
let provider: ethers.JsonRpcProvider,
  firstSigner: ethers.Wallet,
  sponsorSigner: ethers.Wallet,
  targetAddress: string,
  usdcAddress: string,
  recipientAddress: string;

async function initializeSigners() {
  // 检查环境变量
  if (
    !process.env.FIRST_PRIVATE_KEY ||
    !process.env.SPONSOR_PRIVATE_KEY ||
    !process.env.DELEGATION_CONTRACT_ADDRESS ||
    !process.env.QUICKNODE_URL ||
    !process.env.USDC_ADDRESS
  ) {
    console.error("请在 .env 文件中设置你的环境变量。");
    process.exit(1);
  }

  const quickNodeUrl = process.env.QUICKNODE_URL;
  provider = new ethers.JsonRpcProvider(quickNodeUrl);

  firstSigner = new ethers.Wallet(process.env.FIRST_PRIVATE_KEY, provider);
  sponsorSigner = new ethers.Wallet(process.env.SPONSOR_PRIVATE_KEY, provider);

  targetAddress = process.env.DELEGATION_CONTRACT_ADDRESS;
  usdcAddress = process.env.USDC_ADDRESS;
  recipientAddress =
    (await provider.resolveName("vitalik.eth")) ||
    "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";

  console.log("第一个签名者地址:", firstSigner.address);
  console.log("赞助者签名者地址:", sponsorSigner.address);

  // 检查余额
  const firstBalance = await provider.getBalance(firstSigner.address);
  const sponsorBalance = await provider.getBalance(sponsorSigner.address);
  console.log("第一个签名者余额:", ethers.formatEther(firstBalance), "ETH");
  console.log(
    "赞助者签名者余额:",
    ethers.formatEther(sponsorBalance),
    "ETH"
  );
}

async function checkDelegationStatus(address = firstSigner.address) {
  console.log("\n=== 正在检查委托状态 ===");

  try {
    // 获取 EOA 地址的代码
    const code = await provider.getCode(address);

    if (code === "0x") {
      console.log(`❌ 未找到 ${address} 的委托`);
      return null;
    }

    // 检查它是否是 EIP-7702 委托 (以 0xef0100 开头)
    if (code.startsWith("0xef0100")) {
      // 提取委托的地址 (删除 0xef0100 前缀)
      const delegatedAddress = "0x" + code.slice(8); // 删除 0xef0100 (8 个字符)

      console.log(`✅ 找到 ${address} 的委托`);
      console.log(`📍 委托给:${delegatedAddress}`);
      console.log(`📝 完整委托代码:${code}`);

      return delegatedAddress;
    } else {
      console.log(`❓ 地址有代码但不是 EIP-7702 委托:${code}`);
      return null;
    }
  } catch (error) {
    console.error("检查委托状态时出错:", error);
    return null;
  }
}

// 步骤 2:为 EOA 创建授权
// 步骤 3:发送非赞助的 EIP-7702 交易
// 步骤 4:发送赞助的 EIP-7702 交易
// 步骤 5:检查 USDC 余额
// 步骤 6:撤销委托
// 步骤 7:运行完整的工作流程

完成第一步后,在以下部分中将代码片段添加到 index.ts 文件中以实现剩余步骤。在本节末尾,你还将找到完整的代码示例。

步骤 2:为 EOA 创建授权

接下来,我们需要为 EOA 创建授权。此授权将在后续交易中使用,以将执行委托给指定的合约。createAuthorization 函数接受一个 nonce 参数。由于我们首先发送一个非赞助交易,因此在调用此函数时,我们将使用 current_nonce + 1

Nonce 和链 ID

你可以选择在授权交易时指定 noncechainId。在本示例中,我们将显式设置 nonce。如果你也想指定 chainId,请随意取消注释相应的行。否则,Ethers.js 将自动为你检测合适的链 ID。

async function createAuthorization(nonce: number) {
  const auth = await firstSigner.authorize({
    address: targetAddress,
    nonce: nonce,
    // chainId: 11155111, // Sepolia 链 ID
  });

  console.log("使用以下 nonce 创建授权:", auth.nonce);
  return auth;
}

步骤 3:发送非赞助的 EIP-7702 交易

在此交易中,你的 EOA 既授权委托又发送交易,并支付自己的 gas 费用。

此函数创建一个具有 nonce + 1 的授权以将执行委托给指定的合约,然后发送一个交易以在同一交易中转移 0.001 ETH 和 0.002 ETH。type: 4 表示 EIP-7702 交易,authorizationList 包含在上一步中创建的授权。

请注意,我们如何创建指向 EOA 地址 而不是实现合约地址的合约实例。

async function sendNonSponsoredTransaction() {
  console.log("\n=== 交易 1:非赞助 (ETH 转移) ===");

  const currentNonce = await firstSigner.getNonce();
  console.log("第一个签名者的当前 nonce:", currentNonce);

  // 为同一钱包交易创建具有递增 nonce 的授权
  const auth = await createAuthorization(currentNonce + 1);

  // 准备 ETH 转移的调用
  const calls = [\
    // to address, value, data\
    [ethers.ZeroAddress, ethers.parseEther("0.001"), "0x"],\
    [recipientAddress, ethers.parseEther("0.002"), "0x"],\
  ];

  // 创建合约实例并执行
  const delegatedContract = new ethers.Contract(
    firstSigner.address,
    contractABI,
    firstSigner
  );

  const tx = await delegatedContract["execute((address,uint256,bytes)[])"](
    calls,
    {
      type: 4,
      authorizationList: [auth],
    }
  );

  console.log("已发送非赞助交易:", tx.hash);

  const receipt = await tx.wait();
  console.log("非赞助交易的回执:", receipt);

  return receipt;
}

步骤 4:发送赞助的 EIP-7702 交易

赞助交易表示不同的钱包(赞助商)支付 gas 费用,同时代表 EOA 所有者执行操作的交易。这实现了 gasless 用户体验。

赞助交易流程更加复杂,因为它需要签名验证。EOA 所有者必须签署预期操作的摘要,证明他们授权赞助商代表他们执行这些特定调用。否则,任何人都可以代表 EOA 执行任意交易,这将是一个安全风险。此签名将在执行赞助交易之前由委托合约解码和验证。

此函数发送一个赞助交易以在同一交易中将 0.1 USDC 和 0.001 ETH 转移到接收者地址,赞助商支付 gas 费用。

// 用于为赞助调用创建签名的函数,它在实现合约中是必需的
async function createSignatureForCalls(calls: any[], contractNonce: number) {
  // 对签名调用进行编码
  let encodedCalls = "0x";
  for (const call of calls) {
    const [to, value, data] = call;
    encodedCalls += ethers
      .solidityPacked(["address", "uint256", "bytes"], [to, value, data])
      .slice(2);
  }

  // 创建需要签名的摘要
  const digest = ethers.keccak256(
    ethers.solidityPacked(["uint256", "bytes"], [contractNonce, encodedCalls])
  );

  // 使用 EOA 的私钥签署摘要
  return await firstSigner.signMessage(ethers.getBytes(digest));
}

async function sendSponsoredTransaction() {
  console.log("\n=== 交易 2:赞助 (合约函数调用) ===");

  // 准备 ERC20 转移调用数据
  const erc20ABI = [\
    "function transfer(address to, uint256 amount) external returns (bool)",\
  ];
  const erc20Interface = new ethers.Interface(erc20ABI);

  const calls = [\
    [\
      usdcAddress,\
      0n,\
      erc20Interface.encodeFunctionData("transfer", [\
        recipientAddress,\
        ethers.parseUnits("0.1", 6), // 0.1 USDC\
      ]),\
    ],\
    [recipientAddress, ethers.parseEther("0.001"), "0x"],\
  ];

  // 为赞助交易创建合约实例
  const delegatedContract = new ethers.Contract(
    firstSigner.address,
    contractABI,
    sponsorSigner
  );

  // 获取合约 nonce 并创建签名
  const contractNonce = await delegatedContract.nonce();
  const signature = await createSignatureForCalls(calls, contractNonce);

  await checkUSDCBalance(firstSigner.address, "第一个签名者 (发送者)");

  // 执行赞助交易
  const tx = await delegatedContract[\
    "execute((address,uint256,bytes)[],bytes)"\
  ](calls, signature, {
    // type: 4,                   // 重用现有委托。
    // authorizationList: [auth], // 不需要新授权或 EIP-7702 类型。
  });

  console.log("已发送赞助交易:", tx.hash);

  const receipt = await tx.wait();
  console.log("赞助交易的回执:", receipt);

  // 交易后检查 USDC 余额
  console.log("\n--- 交易后 USDC 余额 ---");
  await checkUSDCBalance(firstSigner.address, "第一个签名者 (发送者)");

  return receipt;
}

步骤 5:检查 USDC 余额

要验证你的交易是否正常工作,请在交易前后检查你的 EOA 的 USDC 余额。

async function checkUSDCBalance(address: string, label = "地址") {
  const usdcContract = new ethers.Contract(
    usdcAddress,
    ["function balanceOf(address owner) view returns (uint256)"],
    provider
  );

  try {
    const balance = await usdcContract.balanceOf(address);
    const formattedBalance = ethers.formatUnits(balance, 6); // USDC 有 6 位小数
    console.log(`${label} USDC 余额:${formattedBalance} USDC`);
    return balance;
  } catch (error) {
    console.error(`获取 ${label} 的 USDC 余额时出错:`, error);
    return 0n;
  }
}

步骤 6:撤销委托

你的 EOA 将保持委托给指定的合约,直到你明确更改或撤销它。你可以通过使用零地址授权发送 EIP-7702 交易来撤销委托,以将你的 EOA 恢复到其原始状态。

async function revokeDelegation() {
  console.log("\n=== 正在撤销委托 ===");

  const currentNonce = await firstSigner.getNonce();
  console.log("撤销的当前 nonce:", currentNonce);

  // 创建授权以撤销 (将地址设置为零地址)
  const revokeAuth = await firstSigner.authorize({
    address: ethers.ZeroAddress, // 零地址以撤销
    nonce: currentNonce + 1,
    // chainId: 11155111,
  });

  console.log("已创建撤销授权");

  // 发送带有撤销授权的交易
  const tx = await firstSigner.sendTransaction({
    type: 4,
    to: firstSigner.address,
    authorizationList: [revokeAuth],
  });

  console.log("已发送撤销交易:", tx.hash);

  const receipt = await tx.wait();
  console.log("委托已成功撤销!");

  return receipt;
}

步骤 7:运行完整的工作流程

将所有步骤合并到一个主函数中以执行交易。

async function sendEIP7702Transactions() {
  try {
    // 初始化签名者并获取初始余额
    await initializeSigners();
    await provider.getBalance(firstSigner.address);
    await provider.getBalance(sponsorSigner.address);

    // 在开始之前检查委托
    await checkDelegationStatus();

    // 执行交易
    const receipt1 = await sendNonSponsoredTransaction();

    // 在第一次交易后检查委托
    await checkDelegationStatus();

    const receipt2 = await sendSponsoredTransaction();

    console.log("\n=== 成功 ===");
    console.log("两个 EIP-7702 交易均已成功完成!");
    console.log("非赞助交易区块:", receipt1.blockNumber);
    console.log("赞助交易区块:", receipt2.blockNumber);

    // 如果你想在最后撤销委托,请取消注释
    // await revokeDelegation();

    return { receipt1, receipt2 };
  } catch (error) {
    console.error("EIP-7702 交易中出错:", error);
    throw error;
  }
}

// 执行主函数
sendEIP7702Transactions()
  .then(() => {
    console.log("流程已成功完成。");
  })
  .catch((error) => {
    console.error("无法发送 EIP-7702 交易:", error);
  });

完整代码示例

单击以查看完整代码示例

index.ts

import dotenv from "dotenv";
import { ethers } from "ethers";
import { contractABI } from "./contract";

dotenv.config();

// 用于可重用性的全局变量
let provider: ethers.JsonRpcProvider,
  firstSigner: ethers.Wallet,
  sponsorSigner: ethers.Wallet,
  targetAddress: string,
  usdcAddress: string,
  recipientAddress: string;

async function initializeSigners() {
  // 检查环境变量
  if (
    !process.env.FIRST_PRIVATE_KEY ||
    !process.env.SPONSOR_PRIVATE_KEY ||
    !process.env.DELEGATION_CONTRACT_ADDRESS ||
    !process.env.QUICKNODE_URL ||
    !process.env.USDC_ADDRESS
  ) {
    console.error("请在 .env 文件中设置你的环境变量。");
    process.exit(1);
  }

  const quickNodeUrl = process.env.QUICKNODE_URL;
  provider = new ethers.JsonRpcProvider(quickNodeUrl);

  firstSigner = new ethers.Wallet(process.env.FIRST_PRIVATE_KEY, provider);
  sponsorSigner = new ethers.Wallet(process.env.SPONSOR_PRIVATE_KEY, provider);

  targetAddress = process.env.DELEGATION_CONTRACT_ADDRESS;
  usdcAddress = process.env.USDC_ADDRESS;
  recipientAddress =
    (await provider.resolveName("vitalik.eth")) ||
    "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045";

  console.log("第一个签名者地址:", firstSigner.address);
  console.log("赞助者签名者地址:", sponsorSigner.address);

  // 检查余额
  const firstBalance = await provider.getBalance(firstSigner.address);
  const sponsorBalance = await provider.getBalance(sponsorSigner.address);
  console.log("第一个签名者余额:", ethers.formatEther(firstBalance), "ETH");
  console.log(
    "赞助者签名者余额:",
    ethers.formatEther(sponsorBalance),
    "ETH"
  );
}

async function checkDelegationStatus(address = firstSigner.address) {
  console.log("\n=== 正在检查委托状态 ===");

  try {
    // 获取 EOA 地址的代码
    const code = await provider.getCode(address);

    if (code === "0x") {
      console.log(`❌ 未找到 ${address} 的委托`);
      return null;
    }

    // 检查它是否是 EIP-7702 委托 (以 0xef0100 开头)
    if (code.startsWith("0xef0100")) {
      // 提取委托的地址 (删除 0xef0100 前缀)
      const delegatedAddress = "0x" + code.slice(8); // 删除 0xef0100 (8 个字符)

      console.log(`✅ 找到 ${address} 的委托`);
      console.log(`📍 委托给:${delegatedAddress}`);
      console.log(`📝 完整委托代码:${code}`);

      return delegatedAddress;
    } else {
      console.log(`❓ 地址有代码但不是 EIP-7702 委托:${code}`);
      return null;
    }
  } catch (error) {
    console.error("检查委托状态时出错:", error);
    return null;
  }
}

async function createAuthorization(nonce: number) {
  const auth = await firstSigner.authorize({
    address: targetAddress,
    nonce: nonce,
    // chainId: 11155111, // Sepolia 链 ID
  });

  console.log("使用以下 nonce 创建授权:", auth.nonce);
  return auth;
}

async function sendNonSponsoredTransaction() {
  console.log("\n=== 交易 1:非赞助 (ETH 转移) ===");

  const currentNonce = await firstSigner.getNonce();
  console.log("第一个签名者的当前 nonce:", currentNonce);

  // 为同一钱包交易创建具有递增 nonce 的授权
  const auth = await createAuthorization(currentNonce + 1);

  // 准备 ETH 转移的调用
  const calls = [\
    // to address, value, data\
    [ethers.ZeroAddress, ethers.parseEther("0.001"), "0x"],\
    [recipientAddress, ethers.parseEther("0.002"), "0x"],\
  ];

  // 创建合约实例并执行
  const delegatedContract = new ethers.Contract(
    firstSigner.address,
    contractABI,
    firstSigner
  );

  const tx = await delegatedContract["execute((address,uint256,bytes)[])"](
    calls,
    {
      type: 4,
      authorizationList: [auth],
    }
  );

  console.log("已发送非赞助交易:", tx.hash);

  const receipt = await tx.wait();
  console.log("非赞助交易的回执:", receipt);

  return receipt;
}

// 用于为赞助调用创建签名的函数,它在实现合约中是必需的
async function createSignatureForCalls(calls: any[], contractNonce: number) {
  // 对签名调用进行编码
  let encodedCalls = "0x";
  for (const call of calls) {
    const [to, value, data] = call;
    encodedCalls += ethers
      .solidityPacked(["address", "uint256", "bytes"], [to, value, data])
      .slice(2);
  }

  // 创建需要签名的摘要
  const digest = ethers.keccak256(
    ethers.solidityPacked(["uint256", "bytes"], [contractNonce, encodedCalls])
  );

  // 使用 EOA 的私钥签署摘要
  return await firstSigner.signMessage(ethers.getBytes(digest));
}

async function sendSponsoredTransaction() {
  console.log("\n=== 交易 2:赞助 (合约函数调用) ===");

  // 准备 ERC20 转移调用数据
  const erc20ABI = [\
    "function transfer(address to, uint256 amount) external returns (bool)",\
  ];
  const erc20Interface = new ethers.Interface(erc20ABI);

  const calls = [\
    [\
      usdcAddress,\
      0n,\
      erc20Interface.encodeFunctionData("transfer", [\
        recipientAddress,\
        ethers.parseUnits("0.1", 6), // 0.1 USDC\
      ]),\
    ],\
    [recipientAddress, ethers.parseEther("0.001"), "0x"],\
  ];

  // 为赞助交易创建合约实例
  const delegatedContract = new ethers.Contract(
    firstSigner.address,
    contractABI,
    sponsorSigner
  );

  // 获取合约 nonce 并创建签名
  const contractNonce = await delegatedContract.nonce();
  const signature = await createSignatureForCalls(calls, contractNonce);

  await checkUSDCBalance(firstSigner.address, "第一个签名者 (发送者)");

  // 执行赞助交易
  const tx = await delegatedContract[\
    "execute((address,uint256,bytes)[],bytes)"```
// 执行 main 函数
sendEIP7702Transactions()
  .then(() => {
    console.log("流程成功完成。");
  })
  .catch((error) => {
    console.error("发送 EIP-7702 交易失败:", error);
  });

// 取消注释以运行独立的撤销函数
// revokeDelegationStandalone().catch((error) => {
//   console.error("撤销授权失败:", error);
// });

第八步:运行代码

要运行代码,请在你的终端中执行以下命令:

tsx index.ts

复查结果

运行代码后,你应该看到类似于以下内容的输出:

EIP-7702 交易结果 - Ethers.js

让我们分解输出的关键部分:

授权状态: 一开始,系统检查“第一个签名者”是否具有授权(与其 EOA 关联的智能合约账户)。结果显示:

No delegated contract found for 0x5DfD0ec499A16F2a0f529f16fcE06bbaAb4ef8F8 这证实了最初,“第一个签名者” 只是一个普通的 EOA(外部所有账户)。

非赞助交易: 第一笔交易是“第一个签名者”直接发送的批量交易。它:

  • 附加了一个 authorization 字段,表明它是一个 EIP-7702 交易。
  • 向两个接收者发送 0.001 ETH 和 0.002 ETH。
  • 由“第一个签名者”发起和支付,没有外部赞助。

授权合约部署: 第一笔交易后,系统检测到现在已经部署了一个授权合约(智能账户):

Delegated to: 0x6C2cE0d8c9d4f45e2bb78a44bac4e8b7c

赞助交易(智能账户调用): 第二笔交易是赞助交易,意思是:

  • 它代表“第一个签名者”发起,但由赞助者地址发送和支付。
  • 该交易发送 0.1 USDC 和 0.001 ETH。
  • 即使不再需要单独的身份验证 payload,此交易也使用 EIP-7702 功能。

最终余额检查: 脚本确认更新后的余额,显示:

First Signer (Sender) - USDC Balance: 0.4 USDC

故障排除

在实施 EIP-7702 交易时,你可能会遇到几个常见问题。 了解这些问题及其解决方案将帮助你构建更强大的应用程序。

函数签名歧义

如果你的授权合约具有多个同名的函数(即,execute),则 Ethers.js 可能不知道要调用哪个函数。 这会表现为“ambiguous function description”错误。

// 问题:Ethers.js 无法确定要使用哪个 execute 函数
const tx = await contract.execute(calls); // ❌ 歧义

// 解决方案:使用特定的函数签名
const tx = await contract["execute((address,uint256,bytes)[])"](calls); // ✅ 特定
对象与数组参数不匹配

我们在本指南中使用的 EIP-7702 合约期望元组参数为数组,而不是 JavaScript 对象。

// 问题:对调用参数使用对象
const calls = [\
  { to: "0x123...", value: 100n, data: "0x" } // ❌ 对象格式\
];

// 解决方案:按正确的顺序使用数组
const calls = [\
  ["0x123...", 100n, "0x"] // ✅ 匹配(address,uint256,bytes)的数组格式\
];
Nonce 管理

Nonce 管理对于 EIP-7702 交易至关重要。 以下是一些常见问题及其解决方案:

// 对于同钱包交易(非赞助)
const currentNonce = await signer.getNonce();
const auth = await signer.authorize({
  nonce: currentNonce + 1 // ✅ 为同一钱包递增
});

// 对于不同钱包的交易(赞助)
const auth = await signer.authorize({
  nonce: currentNonce // ✅ 对不同的钱包使用当前的 nonce
});
签名验证

发送受资助的交易时,请确保正确形成签名。 签名必须与预期操作的摘要匹配。

// 确保你的签名创建与合约的预期值匹配
const digest = ethers.keccak256(
  ethers.solidityPacked(
    ["uint256", "bytes"], // 必须与合约的预期值匹配
    [contractNonce, encodedCalls]
  )
);

// 使用 EOA 的密钥签名,而不是赞助者的密钥
const signature = await eoaSigner.signMessage(ethers.getBytes(digest));

结论

你已使用 Ethers.js 成功发送了 EIP-7702 交易,从而启用了高级功能,如批量交易和 Gas 赞助。

虽然它具有开创性,但 EIP-7702 相关的改进仍在发展中。 在你继续探索 EIP-7702 时,请考虑以下事项:

  • 保持更新:关注 EIP-7702 及相关标准的最新发展。 在 X 上关注我们以获取更新,并加入我们的 Discord 服务器以与社区互动。
  • 实验:尝试不同的交易类型,例如具有多个赞助商的赞助交易或复杂的批量操作。
  • 贡献:如果你发现错误或有改进的想法,请考虑为 EIP-7702 讨论或实施做出贡献。
  • 提供反馈:在下面的表格中与我们分享你的经验和想法。 你的反馈有助于我们改进我们的指南和资源。
我们 ❤️ 反馈!

如果你有任何反馈或对新主题的要求,请 告诉我们。 我们很乐意听取你的意见。

更多资源

  • 原文链接: quicknode.com/guides/eth...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。