如何使用 Delegation Toolkit 在您的 dapp 中构建社交邀请功能

使用 Delegation Toolkit 在您的 dapp 中构建社交邀请功能

5 分钟
如何使用 Delegation Toolkit 在您的 dapp 中构建社交邀请功能

在我们之前的博客文章什么是 Delegation Toolkit,您可以用它构建什么?中,我们探讨了 MetaMask 的 Delegation Toolkit 如何帮助开发者构建更灵活、更易用的 dapp。其中一个引人注目的应用场景是构建社交邀请功能,让用户能够以极低的门槛引导朋友加入。

本教程将介绍如何实现这类流程——现有用户可以将有限权限委托给他人,让新用户无需经历繁琐的钱包创建和充值流程,即可轻松探索 dapp。

设想这样一个场景:Alice 想让 Bob 体验她正在使用的 dapp。她向他发送一个邀请,允许他在限定时间内从她的钱包中领取少量 ETH,比如 0.001 ETH。Bob 可以立即开始使用该 dapp,无需安装钱包插件或自行支付 gas 费。与此同时,Alice 对自己的钱包保持完全控制,私钥也不会暴露。

这正是 Delegation Toolkit 所能实现的流畅入门体验——通过权限委托和安全的链下签名。下面我们来快速梳理一下将要构建的内容结构。

邀请方(Inviter)

邀请方是希望共享钱包有限访问权限以帮助他人入门的用户。在本教程中,邀请方将使用 MetaMask Delegator 账户,这是一种基于 EIP-4337 的智能合约钱包。您可以创建新的 delegator 账户,也可以使用已有的账户。在部署和基础设施方面,我们将使用 Pimlico 的 EIP-4337 服务。

邀请(Invitation)

在此场景中,邀请本质上是一个委托(delegation)——由 delegator 账户创建并存储在链下的签名对象。一旦 delegator 账户部署完成并充入 ETH,您就可以创建一个委托,允许另一位用户在特定时间窗口内领取一定数量的代币。这种设置无需被邀请方预先充值钱包或支付 gas,大幅降低了入门门槛。

为简化操作,我们将使用所谓的开放委托(open delegation)。这意味着任何持有该签名委托的人都可以兑换它。由于不绑定特定接收方,无需管理或暴露邀请方的私钥。

被邀请方(Invitee)

被邀请方是正在入门的用户。根据您的实现方式,他们可以使用外部拥有账户(EOA)或智能合约账户(SCA)。在本教程中,当被邀请方点击领取链接时,我们将自动为其生成一个 EOA。在生产环境的 dapp 中,理想情况下您应检测被邀请方是否已连接钱包并优先使用该钱包,但实现这一逻辑超出了本指南的范围。

条件(Conditions)

每个邀请都可以包含条件,以控制其使用方式和时机。这些条件也称为caveats,可以指定资金上限、兑换截止时间,或限制委托的使用次数。在我们的案例中,我们将委托设置为仅可兑换一次,并设定最大代币数量(X)和时间窗口(Y),以确保基本安全性。

接受邀请

当被邀请方点击领取链接并接受邀请时,即完成了委托的兑换。此过程会在区块链上触发一个用户操作,以转移委托的资金。我们将再次借助 Pimlico 的基础设施来发送此操作并完成整个流程。

了解了整体流程后,让我们开始实现吧。

前提条件

在开始构建之前,请确保已完成以下准备工作:

  • Node.js(版本 18 或更高)

  • 用于 Sepolia 测试网的 Infura RPC 端点

  • 对 EIP-4337 账户抽象的基本了解,包括 Paymaster、Bundler 和 User Operations 等核心概念

  • Pimlico Bundler 和 Paymaster URL

安装依赖

首先,安装 delegation toolkit:

npm install @metamask/delegation-toolkit

接下来,安装 Viem 以处理所有以太坊交互:

npm install viem

创建邀请方账户

邀请方账户是共享有限权限以帮助新用户入门的钱包。它应持有足够的加密货币,以覆盖被邀请方在初次与 dapp 交互时产生的 gas 费。在本教程中,我们将使用智能合约账户(通常称为 Gator 账户)来承担这一角色。

值得一提的是,得益于 Pectra 升级,支持 EIP-7702 的普通 EOA 现在也可以委托权限。这为传统钱包提供了参与权限共享的可能性,无需切换至智能合约账户。我们将在另一篇文章中深入探讨这种方式。

首先,在项目根目录创建一个名为 index.ts 的文件,并添加以下代码片段:

import { http, createPublicClient } from "viem";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import { sepolia as chain } from "viem/chains";
import {
  Implementation,
  toMetaMaskSmartAccount,
} from "@metamask/delegator-core-viem";

const RPCEndpoint = "<INFURA RPC ENDPOINT HERE>";
const transport = http(RPCEndpoint);
const publicClient = createPublicClient({ transport, chain });

const privateKey = generatePrivateKey();
const owner = privateKeyToAccount(privateKey);

const deploySalt = "0x";

const account = await toMetaMaskSmartAccount({
  client: publicClient,
  implementation: Implementation.Hybrid,
  deployParams: [owner.address, [], [], []],
  deploySalt,
  signatory: { account: owner },
});
复制

创建 delegator 账户时,有两种主要选项:

  • 混合账户(Hybrid account):此类型只需一个签名方,可以是 EOA 或任何兼容 P256 的签名者。通过 implementation: Implementation.Hybrid 指定。

  • 多签账户(Multisig account):此选项需要多个 EOA 代表 delegator 账户进行签名。通过 implementation: Implementation.MultiSig 指定。

在本教程中,我们使用混合账户,并将签名方设置为普通 EOA。请注意,虽然上面的示例中我们生成了一个私钥,但实际上可以通过编程方式调整为使用现有钱包的私钥。

创建 delegator 账户后,有一个重要细节需要注意:使用 toMetaMaskSmartAccount 创建的账户是反事实的(counterfactual)。这意味着该账户在部署之前并不存在于链上。您仍然可以通过 account.address 获取账户地址,但在区块浏览器上暂时无法找到它。

不过,您仍然可以提前向该地址发送代币。一旦账户部署完成,这些代币即可使用,与该地址相关的任何待处理操作也可立即执行。

部署邀请方 Delegator 账户

为了继续推进,我们需要部署这个反事实账户,使其能够与区块链进行交互。在大多数 dapp 中,此步骤通常会在用户入门时触发,例如用户登录或注册时。

为处理部署,我们将使用 Pimlico 的基础设施。前往您的 Pimlico 控制台,创建一个 API 密钥并复制提供的 URL。该 URL 同时用作 bundler(转发用户操作)和 paymaster(赞助 gas 费)。

由于没有专门用于部署反事实账户的函数,我们将通过向 bundler 提交一个虚拟用户操作来触发部署。该操作本身不会成功,但 Pimlico 会在尝试执行之前先部署账户,这正是我们所需要的。

设置 Pimlico bundler 和 paymaster

安装 @pimlico/permissionless 包以与 Pimlico 的 bundler、paymaster 和用户操作工具进行交互:

npm install permissionless

接下来,我们将配置 Pimlico bundler 和 paymaster,并继续部署 Gator 账户:

import {
  createBundlerClient,
  createPaymasterClient,
} from "viem/account-abstraction";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { zeroAddress } from "viem";

const pimlicoURL = process.env.PIMLICO_URL;

  const paymasterClient = createPaymasterClient({
    transport: http(pimlicoURL),
  });

  const bundlerClient = createBundlerClient({
    transport: http(pimlicoURL),
    paymaster: paymasterClient,
  });

  const pimlicoClient = createPimlicoClient({
    transport: http(pimlicoURL),
  });;

// deploy the gator account
const { fast: gasPrice } = await pimlicoClient.getUserOperationGasPrice();
const isDeployed = await account.isDeployed();
if (!isDeployed) {
  const hash = await bundlerClient.sendUserOperation({
    account,
    calls: [{ to: zeroAddress }],
    ...gasPrice,
  });

  const { receipt } = await bundlerClient.waitForUserOperationReceipt({ hash });
}
复制

在上面的代码片段中,我们首先初始化 Pimlico paymaster 和 bundler 客户端,然后初始化 Pimlico 客户端本身。接着检查 Gator 账户是否已部署。如果尚未部署,我们将使用待部署的账户作为发送方,提交一个虚拟用户操作。

一旦用户操作被打包并提交到内存池,Pimlico bundler 将返回一个交易哈希。我们使用该哈希等待交易被挖出并在链上确认。

有关此过程底层工作原理的更多详情,请参阅 Pimlico 文档

最后一步是向新部署的 delegator 账户充入一些测试 ETH。您可以访问 MetaMask Sepolia 水龙头完成此操作。

创建邀请

邀请方的 delegator 账户已充值完毕,下一步是创建并签署一个开放委托。该委托将允许被邀请方从邀请方账户中领取特定数量的测试 ETH。

委托使用 DelegationStruct 类型定义,其结构如下:

export type DelegationStruct = {
  delegate: Hex;             // The address receiving the delegated permission
  delegator: Hex;            // The address granting the permission
  authority: Hex;            // The parent delegation hash, or ROOT_AUTHORITY if it's the root
  caveats: CaveatStruct[];   // Optional rules that limit what the delegate can do
  salt: bigint;              // A random value to avoid hash collisions
  signature: Hex;            // The delegator's signature
};
复制

在本例中,我们将创建一个根委托(root delegation),即委托链中的第一个环节。虽然可以在根委托的基础上构建再委托链,但本教程仅关注根委托本身。

创建根委托有两种方式:

  • 如果您事先不知道被委托方的地址(如本教程),可以创建开放委托,任何账户均可兑换。

  • 如果您知道被委托方的地址,可以通过将 delegate 字段明确设置为该地址来创建封闭委托(closed delegation)。

为了灵活性和简便性,我们采用开放委托方式:

...
import {
...
  createOpenDelegation,
} from "@metamask/delegator-core-viem";

const caveatBuilder = createCaveatBuilder(account.environment);
const caveats = caveatBuilder
  .addCaveat("limitedCalls", 1)
  .addCaveat("nativeTokenTransferAmount", BigInt(1))
  .addCaveat("timestamp", 0, Math.floor(Date.now() / 1000) + 86_400);

const openDelegation = createOpenDelegation({
  from: account.address,
  caveats,
});
复制

在此步骤中,我们添加了三个 caveat 来控制委托的使用方式:

  • 执行次数限制:使用 limitedCalls caveat,限制被委托方代表邀请方执行操作的次数。

  • 代币转账上限:nativeTokenTransferAmount caveat 限制从邀请方账户转出的原生代币总量。

  • 过期时间:添加基于时间戳的 caveat,确保委托在特定时间后失效。在本例中,邀请将在 24 小时后过期。

虽然还有许多其他 caveat 可供选择,但这些已足够满足我们的需求。您可以在文档中找到支持的 caveat 完整列表,如有需要,也可以自定义 caveat。

委托定义完成后,最后一步是签署并存储它。委托必须由 delegator 签署才能生效。MetaMask delegator 账户提供了 signDelegation 方法,我们将使用它来完成签署。

至于存储方式,您可以选择任何您偏好的方法。在本教程中,为简便起见,我们将使用 localStorage。请注意,我们使用 superjson 库的 stringify 方法来序列化委托,因为 JavaScript 内置的 JSON.stringify 不支持 BigInt 值。

npm install superjson
...
import { stringify } from "superjson";


const signature = await account.signDelegation({
  delegation: openDelegation,
});

localStorage.setItem(
  "DELEGATION",
  stringify({ delegation: openDelegation, signature })
);
复制

兑换邀请

现在让我们切换到被邀请方的视角,即将兑换委托并执行操作的用户,例如将代币从邀请方账户转移到自己的账户。

第一步:从存储中获取委托

由于我们将签署的委托存储在本地,被邀请方可以直接从 localStorage 中获取:

import { parse } from "superjson";

const storedData = localStorage.getItem("DELEGATION");

if (!storedData) {
  throw new Error("Delegation not found in storage");
}

const { delegation, signature } = parse(storedData);
复制

第二步:创建执行(execution)

执行代表被委托方被允许在 delegator 账户上执行的具体操作。它必须遵守委托中定义的所有 caveat。

执行对象通常如下所示:

{
  target,   // the address being called as a hex string
  value,    // the value of the call as a bigint
  callData  // the calldata as a hex string
}
复制

为简化操作,您可以使用 SDK 提供的辅助方法 createExecution:

import { createExecution } from "@codefi/delegator-core-viem";

const execution = createExecution(
       target,   // the address being called as a hex string
  	 value,    // the value of the call as a bigint
  	  callData  // the calldata as a hex string
      );
复制

在我们的案例中,由于我们不是调用智能合约,而是简单地将 ETH 从 delegator 转移给被委托方,执行如下所示:

const execution = createExecution(
        delegateAccount.address,
        amount
      );
复制

第三步:通过 UserOperation 提交执行

如果您对账户抽象概念不熟悉,这部分可能稍显复杂,让我们逐步拆解。

从高层次来看,我们的目标是提交一个 UserOperation,其中包含兑换委托所需的所有交易数据。虽然听起来较为抽象,但最终会在链上执行一笔标准的以太坊交易。

该交易将调用 Delegator 智能合约账户上的 redeemDelegations 函数。该函数负责:

  1. 验证委托并校验其 caveat

  2. 确定执行的处理方式(兑换模式)。更多详情请参阅此处

  3. 执行预期操作(在本例中为转移代币)

简而言之,redeemDelegations 会检查委托是否仍然有效,如果一切验证通过,则执行预期操作。

为发送此交易,我们将使用 bundler 客户端提供的 sendUserOperation 方法。该方法抽象了构建和签署 UserOperation 的复杂性:

import { encodeFunctionData } from "viem";
import {
  encodeExecutionCalldatas,
  encodePermissionContexts,
  SINGLE_DEFAULT_MODE,
} from "@codefi/delegator-core-viem";
import { createPimlicoClient } from "permissionless/clients/pimlico";
import { createBundlerClient } from "viem/account-abstraction";

 const bundlerClient = createBundlerClient({
        transport: http(process.env.NEXT_PUBLIC_BUNDLER_URL),
        chain: lineaSepolia,
      });

      const pimlicoClient = createPimlicoClient({
        transport: http(process.env.NEXT_PUBLIC_BUNDLER_URL),
        chain: lineaSepolia,
      });

      const { fast } = await pimlicoClient.getUserOperationGasPrice();


     
  const redeemData = encodeFunctionData({
          abi: delegateAccount.abi,
          functionName: "redeemDelegations",
          args: [
            encodePermissionContexts([delegation]),
            [SINGLE_DEFAULT_MODE],
            encodeExecutionCalldatas([[execution]]),
          ],
        });

      const hash = await bundlerClient.sendUserOperation({
        account: delegateAccount,
        calls: [
          {
            to: delegateAccount.address,
            value: 0n,
            data: redeemData,
          },
        ],
        paymaster: true,
        ...fast,
      });


  await bundlerClient.waitForUserOperationReceipt({
        hash,
      });
复制

AI 翻译。可能包含错误。请务必核实信息。

评价此翻译
  • Kingsley Okonkwo
    Kingsley Okonkwo

    Kingsley 是一位经认证的以太坊区块链开发者,现任 Consensys 技术写作工程师,专注于开发者工具领域。加入 Consensys 之前,Kingsley 曾在 GigsterBraintrust 等多个平台担任自由职业后端开发者。他拥有 5 年软件开发经验,熟练掌握 JavaScript(Node.js 和 React)、GoLang、SQL 及 Docker 等技术工具。Kingsley 擅长撰写分步教程和操作指南,善用最新 Web3 技术帮助开发者构建更优质的 dapp。他目前定居迪拜,业余时间喜欢打篮球和踢足球。

    阅读所有文章