跳至內容

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

使用 Delegation Toolkit 在您的 dapp 中建立社交邀請功能

5 分鐘
如何使用 Delegation Toolkit 在您的 dapp 中建立社交邀請功能

在我們之前的部落格文章 什麼是 Delegation Toolkit,你可以用它建構什麼? 中,我們探討了 MetaMask 的 Delegation Toolkit 如何賦予開發者建構更靈活、更友善的 dapp 的能力。其中一個引人注目的應用場景,是建立社交邀請功能,讓使用者能以最低摩擦的方式引導朋友加入。

本教學將說明如何實作這類流程——現有使用者可以將有限的權限委派給他人,讓新使用者無需面對事先設定並充值錢包的繁瑣步驟,即可輕鬆探索 dapp。

想像這樣的情境:Alice 想讓 Bob 試用她正在使用的 dapp。她傳送一個邀請,讓 Bob 可以在設定的時間限制內,從她的錢包領取少量 ETH,例如 0.001 ETH。Bob 可以立即開始使用 dapp,無需安裝錢包擴充功能或自行支付 gas 費用。與此同時,Alice 對自己的錢包保有完全的控制權,私鑰也不會外洩。

這正是 Delegation Toolkit 透過權限委派與安全的鏈下簽名所實現的流暢上手體驗。讓我們先快速概覽一下我們將要建構的架構。

邀請者(Inviter)

邀請者是希望分享錢包有限存取權限,以協助引導他人加入的使用者。在本教學中,邀請者將使用 MetaMask Delegator 帳戶,這是一種基於 EIP-4337 的智能合約錢包。你可以建立新的 delegator 帳戶,或使用已設定好的現有帳戶。在部署與基礎設施方面,我們將使用 Pimlico 的 EIP-4337 服務。

邀請(Invitation)

在此情境中,邀請就是一個 delegation——由 delegator 帳戶建立並儲存於鏈下的已簽名物件。一旦 delegator 帳戶部署完成並充值 ETH,你就可以建立一個 delegation,允許另一位使用者在特定時間範圍內領取一定數量的代幣。這樣的設計免除了被邀請者事先充值錢包或支付 gas 的需求,讓上手過程更加順暢。

為了簡化流程,我們將使用所謂的「開放式委派(open delegation)」。這表示任何持有已簽名 delegation 的人都可以兌換它。由於它不綁定特定接收者,因此無需管理或揭露邀請者的私鑰。

被邀請者(Invitee)

被邀請者是正在加入的新使用者。根據你的實作方式,他們可以使用外部擁有帳戶(EOA)或智能合約帳戶(SCA)。在本教學中,當被邀請者點擊領取連結時,我們將自動為其生成一個 EOA。在正式環境的 dapp 中,理想做法是偵測被邀請者是否已連接錢包,若有則直接使用,但實作該邏輯超出本指南的範疇。

條件(Conditions)

每個邀請都可以包含條件,以控制其使用方式與時機。這些條件又稱為 caveats,可能規定資金上限、兌換截止日期,或限制 delegation 可使用的次數。在我們的案例中,我們將設定 delegation 只能兌換一次,並設有最大代幣數量(X)和時間窗口(Y),以確保基本的安全性。

接受邀請

當被邀請者點擊領取連結並接受邀請時,即是在兌換 delegation。此過程會在區塊鏈上觸發一個 user operation,以轉移被委派的資金。我們將再次依賴 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(用於轉發 user operations)和 paymaster(用於贊助 gas 費用)。

由於沒有專門用於部署反事實帳戶的函式,我們將透過向 bundler 提交一個虛擬 user operation 來觸發部署。此操作不會成功,但 Pimlico 會在嘗試執行之前先部署帳戶,這正是我們所需要的。

設定 Pimlico bundler 和 paymaster

安裝 @pimlico/permissionless 套件,以與 Pimlico 的 bundler、paymaster 和 user operation 工具互動:

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 帳戶是否已部署。若尚未部署,我們使用尚未部署的帳戶作為發送者,提交一個虛擬 user operation。

一旦 user operation 被打包並提交至 mempool,Pimlico bundler 會回傳一個交易雜湊值。我們使用該雜湊值等待交易被挖掘並在鏈上確認。

如需了解此過程底層運作的更多詳情,請參閱 Pimlico 文件。

最後一步是為你新部署的 delegator 帳戶充值一些測試 ETH。你可以前往 MetaMask Sepolia 水龍頭 完成此操作。

建立邀請

邀請者的 delegator 帳戶已充值完成,下一步是建立並簽署一個開放式委派。此 delegation 將允許被邀請者從邀請者的帳戶領取特定數量的測試 ETH。

Delegation 使用 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 欄位設定為該地址來建立封閉式委派。

為了靈活性與簡便性,我們採用開放式委派方式:

...
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,
});
複製

在此步驟中,我們加入了三個 caveats 來控制 delegation 的使用方式:

  • 執行次數限制:使用 limitedCalls caveat,限制被委派者代表邀請者執行操作的次數。

  • 代幣轉移上限:nativeTokenTransferAmount caveat 限制從邀請者帳戶可轉移的原生代幣總量。

  • 到期時間:我們加入基於時間戳記的 caveat,確保 delegation 在特定時間後失效。在此案例中,邀請將在 24 小時後到期。

雖然還有許多其他 caveats 可以加入,但這些對我們的使用場景已足夠。你可以在文件中找到完整的支援 caveats 列表,如有需要,你也可以定義自訂 caveats。

一旦 delegation 定義完成,最後一步是簽署並儲存它。delegation 必須由委派者簽署才能生效。MetaMask delegator 帳戶提供了 signDelegation 方法,我們將使用它來完成此目的。

至於儲存方式,你可以選擇任何你偏好的方法。在本教學中,我們將保持簡單,使用 localStorage。請注意,我們使用 superjson 函式庫的 stringify 來序列化 delegation,因為 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 })
);
複製

兌換邀請

現在讓我們切換到被邀請者的視角,即將兌換 delegation 並執行操作的使用者,例如將代幣從邀請者的帳戶轉移至自己的帳戶。

步驟 1:從儲存空間取得 delegation

由於我們將已簽署的 delegation 儲存在本地,被邀請者可以直接從 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);
複製

步驟 2:建立執行(execution)

執行(execution)代表被委派者被允許在委派者帳戶上執行的特定操作。它必須遵守 delegation 中定義的所有 caveats。

execution 物件通常如下所示:

{
  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 從委派者轉移至被委派者,execution 如下所示:

const execution = createExecution(
        delegateAccount.address,
        amount
      );
複製

步驟 3:透過 UserOperation 提交執行

如果你對帳戶抽象概念不熟悉,這部分可能稍微複雜,讓我們逐步拆解說明。

從高層次來看,我們的目標是提交一個 UserOperation,其中包含兌換 delegation 所需的所有交易資料。雖然聽起來較為抽象,但這最終將在鏈上執行一筆標準的以太坊交易。

此交易將呼叫 Delegator 智能合約帳戶上名為 redeemDelegations 的函式。此函式負責:

  1. 驗證 delegation 並確認其 caveats 的有效性

  2. 決定執行的處理方式(兌換模式)。更多詳情請見此處

  3. 執行預期的操作(在此案例中為轉移代幣)

簡單來說,redeemDelegations 會檢查 delegation 是否仍然有效,若一切無誤,則繼續執行預期的操作。

為了發送此交易,我們將使用 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 是一位經認證的以太坊區塊鏈開發者,目前擔任 Consensys 的技術寫作員,專注於開發者工具領域。在加入 Consensys 之前,Kingsley 曾在多個平台上擔任自由接案後端開發者,包括 Gigster 和 Braintrust。他擁有 5 年的軟體開發經驗,熟練掌握 JavaScript(Node.js 與 React)、GoLang、SQL 及 Docker 等工具。Kingsley 擅長撰寫步驟式教學與操作指南,善用最新的 Web3 技術,協助開發者社群打造更優質的 dapp。他目前定居於杜拜,閒暇時喜歡打籃球和踢足球。

    閱讀所有文章