本文介绍了如何使用 Ethers.js 实现 EIP-7702 交易,EIP-7702 允许 EOA 临时具有智能合约功能,从而实现批量交易、Gas 赞助和自定义逻辑等功能。文章提供了详细的步骤,包括环境设置、核心概念讲解、代码示例和问题排查,帮助开发者将 EIP-7702 集成到他们的 dApp 中,并介绍了如何撤销授权。
以太坊的 Pectra 升级 引入了 EIP-7702,使外部所有账户 (EOA) 能够暂时采用智能合约功能。这连接了传统钱包和智能合约账户,解锁了批量交易、gas 赞助和自定义逻辑等功能,而无需更改你的地址。
本指南提供了使用 Ethers.js 实现 EIP-7702 交易的分步过程。你将学习发送批量交易、管理赞助交易、撤销授权,以及解决诸如 nonce 管理和授权签名等挑战,确保无缝集成到你的 DApp 中。
0x04
类型)依赖 | 版本 |
---|---|
Node.js | >v20 |
Ethers.js | >6.14.3 |
在深入研究实现之前,了解 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 至关重要。
用户 (EOA) 签署一条授权消息,其中包括链 ID、nonce、委托地址和签名组件(y_parity、r 和 s)。构建此消息时的一个关键细节是如何正确设置 nonce。
在非赞助交易的情况下,即同一账户既发送交易又授权委托,你必须在签名的授权消息中使用该账户的当前 nonce 加一(current_nonce + 1
)。
为什么需要这样做?
正如 EIP-7702 规范中定义的那样:
授权列表在交易的执行部分开始之前处理,但在发送者的 nonce 递增之后处理。
因此,当 EVM 处理授权列表时,它已经递增了发送者的 nonce。在验证期间,它会检查授权的链上 nonce 是否与授权中的 nonce 匹配。
正如我们在上一节中介绍的关键概念一样,让我们开始设置你的开发环境。
首先,你需要一个具有以太坊 Sepolia 端点的 QuickNode 账户。如果你没有,可以在此处创建一个。然后,创建一个新的以太坊 Sepolia 端点,并将提供的 HTTPS URL 放在手边以供以后使用。
要测试 EIP-7702 交易,你需要在钱包中拥有一些 Sepolia ETH 和 USDC(或其他 ERC-20 代币)。
对于 Sepolia ETH,你可以使用 QuickNode 多链水龙头:
注意: 你需要在以太坊主网上至少拥有 0.001 ETH 才能使用 EVM 水龙头。
对于 USDC,你可以使用 Circle 的 Sepolia USDC 水龙头:
首先,如果你没有全局安装 TypeScript 和 tsx,则可以使用以下命令安装它们:
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,请转到 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 交易。我们将介绍以下步骤:
nonce + 1
规则为 EOA 创建授权,该授权将在后续交易中使用。nonce + 1
规则。在此交易中,EOA 将向两个不同的地址发送 0.002 和 0.001 ETH。首先,让我们设置 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 文件中以实现剩余步骤。在本节末尾,你还将找到完整的代码示例。
接下来,我们需要为 EOA 创建授权。此授权将在后续交易中使用,以将执行委托给指定的合约。createAuthorization
函数接受一个 nonce
参数。由于我们首先发送一个非赞助交易,因此在调用此函数时,我们将使用 current_nonce + 1
。
Nonce 和链 ID
你可以选择在授权交易时指定 nonce
和 chainId
。在本示例中,我们将显式设置 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;
}
在此交易中,你的 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;
}
赞助交易表示不同的钱包(赞助商)支付 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;
}
要验证你的交易是否正常工作,请在交易前后检查你的 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;
}
}
你的 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;
}
将所有步骤合并到一个主函数中以执行交易。
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
运行代码后,你应该看到类似于以下内容的输出:
让我们分解输出的关键部分:
授权状态: 一开始,系统检查“第一个签名者”是否具有授权(与其 EOA 关联的智能合约账户)。结果显示:
No delegated contract found for 0x5DfD0ec499A16F2a0f529f16fcE06bbaAb4ef8F8
这证实了最初,“第一个签名者” 只是一个普通的 EOA(外部所有账户)。
非赞助交易: 第一笔交易是“第一个签名者”直接发送的批量交易。它:
authorization
字段,表明它是一个 EIP-7702 交易。授权合约部署: 第一笔交易后,系统检测到现在已经部署了一个授权合约(智能账户):
Delegated to: 0x6C2cE0d8c9d4f45e2bb78a44bac4e8b7c
赞助交易(智能账户调用): 第二笔交易是赞助交易,意思是:
最终余额检查: 脚本确认更新后的余额,显示:
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 管理对于 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 时,请考虑以下事项:
如果你有任何反馈或对新主题的要求,请 告诉我们。 我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!