Zum Inhalt springen

So erstellst du eine Social-Invite-Funktion in deiner dApp mit dem Delegation Toolkit

Erstelle eine soziale Einladefunktion in deiner Dapp mit dem Delegation Toolkit

5 Min.
So erstellst du eine Social-Invite-Funktion in deiner dApp mit dem Delegation Toolkit

In unserem vorherigen Blogbeitrag, What is the Delegation Toolkit and What Can You Build with It?, haben wir untersucht, wie das Delegation Toolkit von MetaMask Entwicklern ermöglicht, flexiblere und benutzerfreundlichere Dapps zu erstellen. Ein überzeugendes Anwendungsbeispiel ist die Entwicklung einer sozialen Einladefunktion, mit der Nutzer ihre Freunde mit minimalem Aufwand onboarden können.

Dieses Tutorial zeigt, wie ein solcher Ablauf implementiert werden kann: Ein bestehender Nutzer kann einem anderen begrenzte Berechtigungen delegieren, sodass neue Nutzer eine Dapp problemlos erkunden können – ohne die üblichen Hürden, eine Wallet einzurichten und vorab zu finanzieren.

Stell dir vor: Alice möchte, dass Bob eine Dapp ausprobiert, die sie selbst nutzt. Sie schickt ihm eine Einladung, die es ihm ermöglicht, einen kleinen ETH-Betrag aus ihrer Wallet zu beanspruchen – zum Beispiel 0,001 ETH – innerhalb eines festgelegten Zeitrahmens. Bob kann die Dapp sofort nutzen, ohne eine Wallet-Erweiterung installieren oder selbst Gas-Gebühren bezahlen zu müssen. Alice behält dabei die volle Kontrolle über ihre Wallet, ohne ihren privaten Schlüssel preiszugeben.

Das ist die Art von reibungslosem Onboarding-Erlebnis, das das Delegation Toolkit ermöglicht – durch permissioned Delegation und sichere Off-Chain-Signaturen. Lass uns kurz die Struktur dessen skizzieren, was wir aufbauen werden.

Einlader

Der Einlader ist der Nutzer, der begrenzten Zugriff auf seine Wallet teilen möchte, um jemand anderen beim Onboarding zu unterstützen. In diesem Tutorial verwendet der Einlader ein MetaMask Delegator-Konto, also eine Smart-Contract-Wallet auf Basis von EIP-4337. Du kannst entweder ein neues Delegator-Konto erstellen oder ein bereits vorhandenes verwenden, falls du bereits eines eingerichtet hast. Für die Bereitstellung und Infrastruktur nutzen wir Pimlicos EIP-4337-Dienste.

Einladung

Eine Einladung ist in diesem Kontext schlicht eine Delegation – ein signiertes Objekt, das vom Delegator-Konto erstellt und Off-Chain gespeichert wird. Sobald das Delegator-Konto bereitgestellt und mit ETH finanziert ist, kannst du eine Delegation erstellen, die einem anderen Nutzer erlaubt, innerhalb eines bestimmten Zeitraums eine bestimmte Menge an Tokens zu beanspruchen. Dieses Setup macht es überflüssig, dass der Eingeladene eine Wallet finanzieren oder vorab Gas-Gebühren bezahlen muss – was das Onboarding deutlich reibungsloser gestaltet.

Der Einfachheit halber verwenden wir eine sogenannte Open Delegation. Das bedeutet, dass jeder, der Zugriff auf die signierte Delegation hat, diese einlösen kann. Da sie nicht an einen bestimmten Empfänger gebunden ist, muss der private Schlüssel des Einladers weder verwaltet noch offengelegt werden.

Eingeladener

Der Eingeladene ist die Person, die ongeboardet wird. Je nach Implementierung kann er entweder ein Externally Owned Account (EOA) oder ein Smart Contract Account (SCA) verwenden. In diesem Tutorial generieren wir automatisch einen EOA für den Eingeladenen, wenn er auf den Claim-Link klickt. In einer Produktions-Dapp würdest du idealerweise erkennen, ob der Eingeladene bereits eine verbundene Wallet hat, und diese stattdessen verwenden – die Implementierung dieser Logik geht jedoch über den Rahmen dieses Leitfadens hinaus.

Bedingungen

Jede Einladung kann Bedingungen enthalten, die steuern, wie und wann sie verwendet werden kann. Diese Bedingungen, auch bekannt als Caveats, können ein Finanzierungslimit, eine Einlösefrist oder Einschränkungen hinsichtlich der Häufigkeit der Nutzung der Delegation festlegen. In unserem Fall setzen wir die Delegation so, dass sie nur einmal eingelöst werden kann, mit einem maximalen Token-Betrag (X) und einem Zeitfenster (Y), um grundlegende Sicherheit zu gewährleisten.

Einladung annehmen

Wenn der Eingeladene auf den Claim-Link klickt und die Einladung annimmt, löst er die Delegation ein. Dieser Vorgang löst eine User Operation auf der Blockchain aus, um die delegierten Mittel zu übertragen. Wir nutzen erneut Pimlicos Infrastruktur, um diese Operation zu senden und den Ablauf abzuschließen.

Jetzt, da du den Ablauf verstehst, können wir mit der Implementierung beginnen.

Voraussetzungen

Bevor wir mit dem Aufbau beginnen, stelle sicher, dass du Folgendes eingerichtet hast:

  • Node.js (Version 18 oder höher)

  • Einen Infura RPC-Endpunkt für das Sepolia-Testnetz

  • Grundlegende Kenntnisse der EIP-4337 Account Abstraction, einschließlich der wichtigsten Konzepte wie Paymaster, Bundler und User Operations

  • Pimlico Bundler- und Paymaster-URLs

Abhängigkeiten installieren

Installiere zunächst das Delegation Toolkit:

npm install @metamask/delegation-toolkit

Installiere anschließend Viem für alle Ethereum-Interaktionen:

npm install viem

Das Einlader-Konto erstellen

Das Einlader-Konto ist die Wallet, die begrenzte Berechtigungen teilt, um einen neuen Nutzer onzuboarden. Es sollte über genügend Krypto verfügen, um die Gas-Gebühren des Eingeladenen bei dessen ersten Interaktionen mit der Dapp zu decken. In diesem Tutorial verwenden wir ein Smart-Contract-Konto – oft als Gator-Konto bezeichnet – für diese Rolle.

Dank des Pectra-Upgrades können reguläre EOAs mit EIP-7702-Unterstützung mittlerweile ebenfalls Berechtigungen delegieren. Dies eröffnet die Möglichkeit, dass traditionelle Wallets an der Berechtigungsteilung teilnehmen können, ohne auf ein Smart-Contract-Konto wechseln zu müssen. Wir werden diesen Ansatz in einem separaten Beitrag ausführlicher behandeln.

Erstelle zunächst eine Datei namens index.ts im Stammverzeichnis deines Projekts und füge die folgenden Code-Snippets hinzu:

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 },
});
Kopieren

Bei der Erstellung eines Delegator-Kontos hast du zwei Hauptoptionen:

  • Hybrid-Konto: Dieser Typ erfordert nur einen einzigen Unterzeichner, der ein EOA oder ein beliebiger P256-kompatibler Signer sein kann. Er wird mit implementation: Implementation.Hybrid angegeben.

  • Multisig-Konto: Diese Option erfordert mehrere EOAs, die im Namen des Delegator-Kontos unterzeichnen. Sie wird mit implementation: Implementation.MultiSig angegeben.

In diesem Tutorial verwenden wir ein Hybrid-Konto und setzen den Unterzeichner als regulären EOA. Beachte, dass wir im obigen Beispiel zwar einen privaten Schlüssel generieren, dieser jedoch programmatisch angepasst werden kann, um den privaten Schlüssel einer bestehenden Wallet zu verwenden.

Nachdem wir das Delegator-Konto erstellt haben, gibt es einen wichtigen Punkt zu beachten: Mit toMetaMaskSmartAccount erstellte Konten sind kontrafaktisch. Das bedeutet, das Konto existiert on-chain erst, wenn es bereitgestellt wird. Du kannst die Kontoadresse zwar über account.address abrufen, aber wenn du sie in einem Block-Explorer suchst, wirst du sie noch nicht finden.

Du kannst jedoch bereits vorab Tokens an diese Adresse senden. Sobald das Konto bereitgestellt ist, sind diese Tokens zugänglich, und alle ausstehenden Aktionen, die mit dieser Adresse verknüpft sind, können sofort ausgeführt werden.

Das Delegator-Konto des Einladers bereitstellen

Um fortzufahren, müssen wir das kontrafaktische Konto bereitstellen, damit es mit der Blockchain interagieren kann. In den meisten Dapps würde dieser Schritt typischerweise beim Onboarding ausgelöst werden, beispielsweise wenn sich ein Nutzer anmeldet oder registriert.

Für die Bereitstellung nutzen wir Pimlicos Infrastruktur. Gehe zu deinem Pimlico-Dashboard, erstelle einen API-Schlüssel und kopiere die bereitgestellte URL. Diese URL dient sowohl als Bundler (zum Weiterleiten von User Operations) als auch als Paymaster (zum Sponsern von Gas-Gebühren).

Da es keine dedizierte Funktion zur Bereitstellung eines kontrafaktischen Kontos gibt, lösen wir die Bereitstellung aus, indem wir eine Dummy-User-Operation an den Bundler senden. Diese Operation wird nicht erfolgreich sein, aber Pimlico wird das Konto bereitstellen, bevor es versucht, sie auszuführen – das ist alles, was wir benötigen.

Pimlico-Bundler und -Paymaster einrichten

Installiere das @pimlico/permissionless-Paket, um mit Pimlicos Bundler, Paymaster und User-Operation-Tools zu interagieren:

npm install permissionless

Als Nächstes konfigurieren wir den Pimlico-Bundler und -Paymaster und fahren mit der Bereitstellung des Gator-Kontos fort:

… 
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 });
}
Kopieren

Im obigen Snippet initialisieren wir zunächst die Pimlico-Paymaster- und Bundler-Clients, gefolgt vom Pimlico-Client selbst. Anschließend prüfen wir, ob das Gator-Konto bereits bereitgestellt wurde. Falls nicht, senden wir eine Dummy-User-Operation mit dem noch nicht bereitgestellten Konto als Sender.

Der Pimlico-Bundler gibt einen Transaktions-Hash zurück, sobald die User Operation gebündelt und an den Mempool übermittelt wurde. Wir verwenden diesen Hash, um auf das Mining und die Bestätigung der Transaktion on-chain zu warten.

Weitere Details zur Funktionsweise dieses Prozesses findest du in der Pimlico-Dokumentation.

Der letzte Schritt besteht darin, dein neu bereitgestelltes Delegator-Konto mit etwas Test-ETH zu finanzieren. Dies kannst du über den MetaMask Sepolia Faucet tun.

Eine Einladung erstellen

Nachdem nun ein finanziertes Delegator-Konto für den Einlader eingerichtet ist, besteht der nächste Schritt darin, eine Open Delegation zu erstellen und zu signieren. Diese Delegation erlaubt dem Eingeladenen, einen bestimmten Betrag an Test-ETH vom Konto des Einladers zu beanspruchen.

Delegationen werden mit dem Typ DelegationStruct definiert, der folgende Struktur aufweist:

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
};
Kopieren

In diesem Fall erstellen wir eine Root-Delegation, also das erste Glied in einer Delegationskette. Obwohl es möglich ist, auf einer Root-Delegation aufzubauen, um eine Kette von Re-Delegationen zu erstellen, konzentriert sich dieses Tutorial ausschließlich auf die Root-Delegation selbst.

Es gibt zwei Möglichkeiten, eine Root-Delegation zu erstellen:

  • Wenn du die Adresse des Delegierten nicht im Voraus kennst (wie in diesem Tutorial), kannst du eine Open Delegation erstellen, die von jedem Konto eingelöst werden kann.

  • Wenn du die Adresse des Delegierten kennst, kannst du eine Closed Delegation erstellen, indem du das Feld delegate explizit auf diese Adresse setzt.

Wir wählen den Open-Delegation-Ansatz für mehr Flexibilität und Einfachheit:

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

In diesem Schritt haben wir drei Caveats hinzugefügt, um zu steuern, wie die Delegation verwendet werden kann:

  • Ausführungslimit: Mit dem limitedCalls-Caveat schränken wir die Anzahl der Aktionen ein, die der Delegierte im Namen des Einladers durchführen kann.

  • Token-Transferlimit: Das nativeTokenTransferAmount-Caveat begrenzt den Gesamtbetrag an nativen Tokens, der vom Konto des Einladers übertragen werden kann.

  • Ablaufdatum: Wir fügen ein zeitstempelbasiertes Caveat hinzu, um sicherzustellen, dass die Delegation nach einem bestimmten Zeitpunkt ungültig wird. In diesem Fall läuft die Einladung nach 24 Stunden ab.

Obwohl es viele weitere Caveats gibt, die du einbeziehen kannst, sind diese für unseren Anwendungsfall ausreichend. Eine vollständige Liste der unterstützten Caveats findest du in der Dokumentation. Bei Bedarf kannst du auch eigene Caveats definieren.

Sobald die Delegation definiert ist, besteht der letzte Schritt darin, sie zu signieren und zu speichern. Damit eine Delegation gültig ist, muss sie vom Delegator signiert werden. Das MetaMask-Delegator-Konto stellt eine signDelegation-Methode bereit, die wir für diesen Zweck verwenden.

Was die Speicherung betrifft, kannst du jede beliebige Methode wählen. In diesem Tutorial halten wir es einfach und verwenden localStorage. Beachte, dass wir stringify aus der superjson-Bibliothek verwenden, um die Delegation zu serialisieren, da JavaScripts eingebautes JSON.stringify keine BigInt-Werte unterstützt.

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


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

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

Einladung einlösen

Wechseln wir nun zur Perspektive des Eingeladenen, also des Nutzers, der die Delegation einlöst und eine Aktion ausführt, beispielsweise das Übertragen von Tokens vom Konto des Einladers auf sein eigenes.

Schritt 1: Delegation aus dem Speicher abrufen

Da wir die signierte Delegation lokal gespeichert haben, kann der Eingeladene sie direkt aus localStorage abrufen:

import { parse } from "superjson";

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

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

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

Schritt 2: Eine Ausführung erstellen

Eine Ausführung repräsentiert die spezifische Aktion, die der Delegierte auf dem Konto des Delegators durchführen darf. Sie muss alle in der Delegation definierten Caveats einhalten.

Ein Ausführungsobjekt sieht typischerweise so aus:

{
  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
}
Kopieren

Der Einfachheit halber kannst du die Hilfsmethode createExecution aus dem SDK verwenden:

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
      );
Kopieren

In unserem Fall rufen wir keinen Smart Contract auf, sondern übertragen lediglich ETH vom Delegator an den Delegierten. Die Ausführung sieht daher so aus:

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

Schritt 3: Die Ausführung mit einer UserOperation einreichen

Dieser Teil kann etwas komplex sein, wenn du neu im Bereich Account Abstraction bist – lass uns ihn daher aufschlüsseln.

Auf hoher Ebene ist unser Ziel, eine UserOperation einzureichen, die alle notwendigen Transaktionsdaten zum Einlösen der Delegation enthält. Auch wenn es abstrakt klingt, resultiert dies letztendlich in einer standardmäßigen Ethereum-Transaktion, die on-chain ausgeführt wird.

Diese Transaktion ruft eine Funktion namens redeemDelegations auf dem Delegator Smart Contract Account auf. Diese Funktion ist verantwortlich für:

  1. Die Überprüfung der Delegation und die Validierung ihrer Caveats

  2. Die Bestimmung, wie die Ausführung verarbeitet werden soll (Redeem-Modus). Mehr dazu hier

  3. Die Ausführung der gewünschten Aktion (in diesem Fall die Übertragung von Tokens)

Vereinfacht gesagt prüft redeemDelegations, ob die Delegation noch gültig ist, und führt, wenn alles stimmt, die beabsichtigte Aktion aus.

Um diese Transaktion zu senden, verwenden wir die sendUserOperation-Methode des Bundler-Clients. Diese abstrahiert die Komplexität der Konstruktion und Signierung der 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,
      });
Kopieren

KI-übersetzt. Kann Fehler enthalten. Bitte überprüfen Sie die Informationen stets.

Bewerten Sie die Übersetzung

Dieser Artikel wurde verfasst von:

  • Kingsley Okonkwo

    Kingsley, zertifizierter Ethereum-Blockchain-Entwickler, ist als technischer Redakteur bei Consensys tätig und hat sich auf Entwickler-Tools spezialisiert. Vor seinem Einstieg bei Consensys arbeitete Kingsley als freiberuflicher Backend-Entwickler auf verschiedenen Plattformen, darunter Gigster und Braintrust. Mit 5 Jahren Erfahrung in der Softwareentwicklung beherrscht er Tools wie JavaScript (Node.js & React), GoLang, SQL und Docker. Kingsley zeichnet sich durch die Erstellung von Schritt-für-Schritt-Tutorials und Anleitungen aus und nutzt dabei die neuesten Web3-Technologien, um anderen Entwicklern beim Aufbau besserer Dapps zu helfen. Derzeit lebt Kingsley in Dubai und verbringt seine Freizeit gerne mit Basketball und Fußball.

    Alle Artikel lesen