Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

使用 EIP-7702 的会话密钥

会话密钥是第二个可以为账户签名的密钥,其权限和有效期由所有者设置。所有者密钥只需签署一次即可授予权限。之后,应用程序、后端或代理即可使用会话密钥发送交易,用户无需批准每笔交易。

本指南在 Stable 测试网上使用 Alchemy Wallet APIs。EOA 委托给 Modular Account v2,这是一个经过审计的智能账户,因此您无需编写或部署委托合约。

工作原理

  1. 委托 EOA。 所有者签署一份 EIP-7702 授权,将 EOA 指向 Modular Account v2。EOA 保留其地址、密钥和余额。
  2. 授予会话。 所有者签署一份会话授予:会话密钥的地址、其权限和有效期。此时链上尚未写入任何内容。
  3. 使用会话密钥发送。 一个仅以会话密钥作为签名的客户端为所有者的账户发送调用。第一个调用会在链上安装会话。

先决条件

  • Node 22 或更高版本。
  • 一个已启用 Stable 测试网的 Alchemy 应用程序,及其 API 密钥。
  • 可选:一个 Alchemy Gas Manager 策略 ID。使用策略,Alchemy 会赞助 Gas。如果没有策略,EOA 将以 USDT0 支付 Gas,并需要一个测试网余额。
npm install @alchemy/wallet-apis viem
added 2 packages

步骤 1:配置客户端

创建一个由所有者签名的客户端。Wallet API 默认以 EIP-7702 模式运行,因此所有者的 EOA 地址就是智能账户地址。

// config.ts
import { alchemyWalletTransport, createSmartWalletClient } from "@alchemy/wallet-apis";
import { createPublicClient, http, type Hex } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { stableTestnet } from "viem/chains";
 
export const chain = stableTestnet;
export const transport = alchemyWalletTransport({ apiKey: process.env.ALCHEMY_API_KEY! });
export const paymaster = process.env.ALCHEMY_GAS_POLICY_ID
  ? { policyId: process.env.ALCHEMY_GAS_POLICY_ID }
  : undefined;
 
export const USDT0_ADDRESS = "0x78Cf24370174180738C5B8E352B6D14c83a6c9A9";
 
export const owner = privateKeyToAccount(process.env.OWNER_PRIVATE_KEY as Hex);
export const account = owner.address;
 
export const ownerClient = createSmartWalletClient({ transport, chain, signer: owner, paymaster });
export const publicClient = createPublicClient({ chain, transport: http() });

步骤 2:委托 EOA

会话只能安装在已委托的账户上。发送一个空的调用集:Wallet API 会检测到缺失的委托,并在同一笔交易中提交 EIP-7702 授权。

// delegate.ts
import { account, ownerClient, publicClient } from "./config";
 
// Delegated EOAs carry the code 0xef0100 || delegate address.
const code = await publicClient.getCode({ address: account });
 
if (code?.startsWith("0xef0100")) {
  console.log("already delegated to", `0x${code.slice(8)}`);
} else {
  const { id } = await ownerClient.sendCalls({ calls: [] });
  const status = await ownerClient.waitForCallsStatus({ id });
  if (status.status !== "success") throw new Error("delegation failed");
  console.log("delegated in tx", status.receipts?.[0]?.transactionHash);
}
delegated in tx 0x...

步骤 3:授予有范围的会话密钥

生成会话密钥并授予最窄的权限,以满足您的用例。此示例允许该密钥在一小时内转移最多 10 个 USDT0。

// grant.ts
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import { parseUnits } from "viem";
import { account, ownerClient, USDT0_ADDRESS } from "./config";
 
// Store this key wherever the app or agent that uses it runs.
export const sessionKey = privateKeyToAccount(generatePrivateKey());
const expirySec = Math.floor(Date.now() / 1000) + 60 * 60;
 
export const permissions = await ownerClient.grantPermissions({
  account,
  expirySec,
  key: { publicKey: sessionKey.address, type: "secp256k1" },
  permissions: [
    {
      type: "erc20-token-transfer",
      data: { address: USDT0_ADDRESS, allowance: parseUnits("10", 6) },
    },
  ],
});
 
console.log("session key", sessionKey.address);
console.log("expires    ", new Date(expirySec * 1000).toISOString());
session key 0x...
expires     2026-10-12T13:00:00.000Z

permissions.context 是授予的证明。会话密钥发送的每次调用都应传递它。

如果没有 Gas 策略赞助 Gas,会话密钥的交易将从账户的 USDT0 中支付 Gas。添加 gas-limit 权限以限制该支出。

权限类型

类型data授予
erc20-token-transferaddress, allowance对一个 ERC-20 代币进行 transfer 和 approve,总计不超过 allowance
native-token-transferallowance本地值转移,总计不超过 allowance
gas-limitlimit会话密钥可以从账户中花费的 Gas
contract-accessaddress某个合约上的所有函数
functions-on-contractaddress, functions某个合约上列出的函数选择器
functions-on-all-contractsfunctions任何合约上列出的函数选择器
account-functionsfunctions账户本身上列出的函数选择器
rootnone所有者可以做的所有事情,直到过期

步骤 4:使用会话密钥发送交易

创建第二个客户端,其唯一的签名者是会话密钥,代表所有者的账户操作。所有者密钥从此处开始不再使用。

// send.ts
import { createSmartWalletClient } from "@alchemy/wallet-apis";
import { encodeFunctionData, erc20Abi, parseUnits } from "viem";
import { account, chain, paymaster, transport, USDT0_ADDRESS } from "./config";
import { permissions, sessionKey } from "./grant";
 
const sessionClient = createSmartWalletClient({ transport, chain, signer: sessionKey, account, paymaster });
 
const { id } = await sessionClient.sendCalls({
  calls: [
    {
      to: USDT0_ADDRESS,
      data: encodeFunctionData({
        abi: erc20Abi,
        functionName: "transfer",
        args: ["0xRecipient...", parseUnits("1", 6)],
      }),
    },
  ],
  capabilities: { permissions },
});
 
const status = await sessionClient.waitForCallsStatus({ id });
if (status.status !== "success") throw new Error("session-key call failed");
console.log("sent in tx", status.receipts?.[0]?.transactionHash);
sent in tx 0x...

超出所授予权限的调用将失败,并且不会转移任何资金。例如:超出剩余限额的转账,或转账其他代币。

结束会话

会话在其 expirySec 时停止工作。保持有效期较短,并在旧会话结束时授予新会话。

清除委托

要完全移除智能账户,所有者将 EOA 重新委托给零地址。EOA 自己发送此交易,并以 USDT0 支付 Gas。

// undelegate.ts
import { createWalletClient, http, zeroAddress } from "viem";
import { chain, owner } from "./config";
 
const walletClient = createWalletClient({ account: owner, chain, transport: http() });
 
const authorization = await walletClient.signAuthorization({
  contractAddress: zeroAddress,
  executor: "self", // the EOA sends the transaction, so the authorization nonce is its nonce + 1
});
 
const hash = await walletClient.sendTransaction({
  to: owner.address,
  authorizationList: [authorization],
  maxPriorityFeePerGas: 0n,
});
console.log("delegation cleared in tx", hash);
delegation cleared in tx 0x...

如果您的 Alchemy 计划包含针对解除委托的赞助策略,ownerClient.undelegateAccount() 会发送相同的重置操作,并由赞助的 Gas 支付。

考量因素

  • 会话密钥是一种凭证。 任何持有它的人都可以在其权限范围内行事,直到过期。像存储其他私钥一样存储它。
  • 委托是持久的。 EOA 运行 Modular Account v2,直到所有者清除或替换委托。使用您愿意升级的密钥进行测试。
  • 每个 EOA 一个委托。 委托给另一个合约,例如订阅管理器,会替换 Modular Account v2 并禁用其会话。

下一步建议