Dans notre précédent article de blog, What is the Delegation Toolkit and What Can You Build with It?, nous avons exploré comment le Delegation Toolkit de MetaMask permet aux développeurs de créer des dapps plus flexibles et conviviales. L'un des cas d'usage les plus intéressants est la création d'une fonctionnalité d'invitation sociale permettant aux utilisateurs d'intégrer leurs amis avec un minimum de friction.
Ce tutoriel explique comment implémenter ce type de flux, dans lequel un utilisateur existant peut déléguer des permissions limitées à un autre, facilitant ainsi l'exploration d'une dapp par les nouveaux utilisateurs sans les obstacles habituels liés à la configuration et à l'alimentation d'un portefeuille au préalable.
Imaginez la situation suivante : Alice souhaite que Bob essaie une dapp qu'elle utilise. Elle lui envoie une invitation lui permettant de réclamer une petite quantité d'ETH depuis son portefeuille, par exemple 0,001 ETH, dans un délai imparti. Bob peut commencer à utiliser la dapp immédiatement, sans avoir besoin d'installer une extension de portefeuille ni de payer lui-même les frais de gas. Pendant ce temps, Alice conserve le contrôle total de son portefeuille, sans aucune exposition de sa clé privée.
C'est le type d'expérience d'intégration simplifiée que le Delegation Toolkit rend possible, grâce à la délégation avec permissions et aux signatures sécurisées hors chaîne. Décrivons rapidement la structure de ce que nous allons construire.
Invitant
L'invitant est l'utilisateur qui souhaite partager un accès limité à son portefeuille afin d'aider à intégrer quelqu'un d'autre. Dans ce tutoriel, l'invitant utilisera un compte MetaMask Delegator, qui est un portefeuille de contrat intelligent basé sur l'EIP-4337. Vous pouvez soit créer un nouveau compte delegator, soit en utiliser un existant si vous en avez déjà configuré un. Pour gérer le déploiement et l'infrastructure, nous utiliserons les services EIP-4337 de Pimlico.
Invitation
Une invitation dans ce contexte est simplement une délégation, un objet signé créé par le compte delegator et stocké hors chaîne. Une fois le compte delegator déployé et alimenté en ETH, vous pouvez créer une délégation permettant à un autre utilisateur de réclamer une certaine quantité de tokens dans un délai spécifique. Cette configuration supprime la nécessité pour l'invité d'alimenter un portefeuille ou de payer des frais de gas à l'avance, ce qui rend l'intégration beaucoup plus fluide.
Pour simplifier les choses, nous utiliserons ce qu'on appelle une délégation ouverte. Cela signifie que toute personne ayant accès à la délégation signée peut la racheter. Comme elle n'est pas liée à un destinataire spécifique, il n'est pas nécessaire de gérer ou d'exposer la clé privée de l'invitant.
Invité
L'invité est la personne en cours d'intégration. Il peut utiliser soit un Externally Owned Account (EOA), soit un Smart Contract Account (SCA), selon votre implémentation. Pour ce tutoriel, nous générerons automatiquement un EOA pour l'invité lorsqu'il cliquera sur le lien de réclamation. Dans une dapp en production, vous détecteriez idéalement si l'invité a déjà un portefeuille connecté et l'utiliseriez à la place, mais l'implémentation de cette logique dépasse le cadre de ce guide.
Conditions
Chaque invitation peut inclure des conditions pour contrôler comment et quand elle peut être utilisée. Ces conditions, également connues sous le nom de caveats, peuvent spécifier une limite de financement, une date limite de rachat, ou des restrictions sur le nombre de fois que la délégation peut être utilisée. Dans notre cas, nous configurerons la délégation pour qu'elle ne soit rachetable qu'une seule fois, avec un montant maximum de tokens (X) et une fenêtre temporelle (Y) pour garantir une sécurité de base.
Accepter l'invitation
Lorsque l'invité clique sur le lien de réclamation et accepte l'invitation, il rachète la délégation. Ce processus déclenche une user operation sur la blockchain pour transférer les fonds délégués. Nous nous appuierons à nouveau sur l'infrastructure de Pimlico pour envoyer cette opération et finaliser le flux.
Maintenant que vous comprenez le flux, commençons l'implémentation.
Prérequis
Avant de commencer à construire, assurez-vous d'avoir les éléments suivants configurés :
Node.js (version 18 ou ultérieure)
Un endpoint RPC Infura pour le testnet Sepolia
Une compréhension de base de l'account abstraction EIP-4337, incluant les concepts clés tels que le Paymaster, le Bundler et les User Operations
Les URLs du Bundler et du Paymaster Pimlico
Installer les dépendances
Commencez par installer le delegation toolkit :
npm install @metamask/delegation-toolkitEnsuite, installez Viem pour toutes les interactions Ethereum :
npm install viemCréer le compte Invitant
Le compte invitant est le portefeuille qui partage des permissions limitées pour aider à intégrer un nouvel utilisateur. Il doit disposer de suffisamment de crypto pour couvrir les frais de gas de l'invité lors de ses premières interactions avec la dapp. Dans ce tutoriel, nous utiliserons un smart contract account, souvent appelé compte Gator, pour remplir ce rôle.
Cela dit, grâce à la mise à niveau Pectra, les EOA classiques avec support EIP-7702 peuvent désormais déléguer des permissions également. Cela ouvre la possibilité pour les portefeuilles traditionnels de participer au partage de permissions sans passer à un smart contract account. Nous approfondirons cette approche dans un article séparé.
Pour commencer, créez un fichier nommé index.ts à la racine de votre projet et ajoutez les extraits de code suivants :
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 },
});
Lors de la création d'un compte delegator, vous avez deux options principales :
Compte hybride : Ce type ne nécessite qu'un seul signataire, qui peut être un EOA ou tout signataire compatible P256. Il est spécifié en utilisant implementation: Implementation.Hybrid.
Compte multisig : Cette option nécessite plusieurs EOA pour signer au nom du compte delegator. Il est spécifié en utilisant implementation: Implementation.MultiSig.
Pour ce tutoriel, nous utilisons un compte hybride et définissons le signataire comme un EOA classique. Notez que bien que nous générions une clé privée dans l'exemple ci-dessus, cela peut être ajusté pour utiliser la clé privée d'un portefeuille existant de manière programmatique.
Maintenant que nous avons créé le compte delegator, voici un détail important à garder à l'esprit : les comptes créés avec toMetaMaskSmartAccount sont contrefactuels. Cela signifie que le compte n'existe pas sur la chaîne tant qu'il n'est pas déployé. Vous pouvez toujours récupérer l'adresse du compte en utilisant account.address, mais si vous essayez de la rechercher sur un explorateur de blocs, vous ne la trouverez pas encore.
Cependant, vous pouvez quand même envoyer des tokens à cette adresse à l'avance. Une fois le compte déployé, ces tokens seront accessibles, et toutes les actions en attente liées à cette adresse pourront être exécutées immédiatement.
Déployer le compte Delegator de l'invitant
Pour avancer, nous devons déployer le compte contrefactuel afin qu'il puisse interagir avec la blockchain. Dans la plupart des dapps, cette étape serait généralement déclenchée lors de l'intégration, par exemple lorsqu'un utilisateur se connecte ou s'inscrit.
Pour gérer le déploiement, nous utiliserons l'infrastructure de Pimlico. Rendez-vous sur votre tableau de bord Pimlico, créez une clé API et copiez l'URL fournie. Cette URL sert à la fois de bundler (pour relayer les user operations) et de paymaster (pour sponsoriser les frais de gas).
Comme il n'existe pas de fonction dédiée au déploiement d'un compte contrefactuel, nous déclencherons le déploiement en soumettant une user operation fictive au bundler. Cette opération ne réussira pas, mais Pimlico déploiera le compte avant de tenter de l'exécuter, ce qui est tout ce dont nous avons besoin.
Configurer le bundler et le paymaster Pimlico
Installez le package @pimlico/permissionless pour interagir avec le bundler, le paymaster et les outils de user operation de Pimlico :
npm install permissionless Ensuite, nous configurerons le bundler et le paymaster Pimlico, puis procéderons au déploiement du compte 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),
});;
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 });
}
Dans l'extrait ci-dessus, nous initialisons d'abord les clients paymaster et bundler Pimlico, puis le client Pimlico lui-même. Nous vérifions ensuite si le compte Gator a déjà été déployé. Si ce n'est pas le cas, nous envoyons une user operation fictive en utilisant le compte non encore déployé comme expéditeur.
Le bundler Pimlico renvoie un hash de transaction une fois que la user operation est regroupée et soumise au mempool. Nous utilisons ce hash pour attendre que la transaction soit minée et confirmée sur la chaîne.
Pour plus de détails sur le fonctionnement de ce processus en coulisses, consultez la documentation Pimlico.
La dernière étape consiste à alimenter votre compte delegator nouvellement déployé avec quelques ETH de test. Vous pouvez le faire en visitant le faucet MetaMask Sepolia.
Créer une invitation
Avec un compte delegator alimenté maintenant configuré pour l'invitant, l'étape suivante consiste à créer et signer une délégation ouverte. Cette délégation permettra à l'invité de réclamer une quantité spécifique d'ETH de test depuis le compte de l'invitant.
Les délégations sont définies à l'aide du type DelegationStruct, qui a la structure suivante :
export type DelegationStruct = {
delegate: Hex;
delegator: Hex;
authority: Hex;
caveats: CaveatStruct[];
salt: bigint;
signature: Hex;
};
Dans ce cas, nous allons créer une délégation racine, qui est le premier maillon d'une chaîne de délégation. Bien qu'il soit possible de s'appuyer sur une délégation racine pour créer une chaîne de re-délégations, ce tutoriel se concentrera uniquement sur la délégation racine elle-même.
Il existe deux façons de créer une délégation racine :
Si vous ne connaissez pas l'adresse du délégué à l'avance (comme dans ce tutoriel), vous pouvez créer une délégation ouverte, qui peut être rachetée par n'importe quel compte.
Si vous connaissez l'adresse du délégué, vous pouvez créer une délégation fermée en définissant explicitement le champ delegate sur cette adresse.
Nous opterons pour l'approche de délégation ouverte pour plus de flexibilité et de simplicité :
...
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,
});
Dans cette étape, nous avons ajouté trois caveats pour contrôler la façon dont la délégation peut être utilisée :
Limite d'exécution : En utilisant le caveat limitedCalls, nous limitons le nombre de fois que le délégué peut effectuer des actions au nom de l'invitant.
Plafond de transfert de tokens : Le caveat nativeTokenTransferAmount limite le montant total de tokens natifs pouvant être transférés depuis le compte de l'invitant.
Expiration : Nous ajoutons un caveat basé sur un horodatage pour garantir que la délégation devient invalide après un certain temps. Dans ce cas, l'invitation expire dans 24 heures.
Bien qu'il existe de nombreux autres caveats que vous pouvez inclure, ceux-ci sont suffisants pour notre cas d'usage. Vous pouvez trouver une liste complète des caveats pris en charge dans la documentation, et si nécessaire, vous pouvez également définir des caveats personnalisés.
Une fois la délégation définie, la dernière étape consiste à la signer et à la stocker. Pour qu'une délégation soit valide, elle doit être signée par le delegator. Le compte delegator MetaMask expose une méthode signDelegation, que nous utiliserons à cet effet.
Concernant le stockage, vous pouvez choisir la méthode que vous préférez. Dans ce tutoriel, nous allons faire simple et utiliser localStorage. Notez que nous utilisons stringify de la bibliothèque superjson pour sérialiser la délégation, car le JSON.stringify intégré de JavaScript ne prend pas en charge les valeurs BigInt.
npm install superjson...
import { stringify } from "superjson";
const signature = await account.signDelegation({
delegation: openDelegation,
});
localStorage.setItem(
"DELEGATION",
stringify({ delegation: openDelegation, signature })
);
Racheter l'invitation
Passons maintenant à la perspective de l'invité, c'est-à-dire l'utilisateur qui va racheter la délégation et effectuer une action, comme transférer des tokens depuis le compte de l'invitant vers le sien.
Étape 1 : Récupérer la délégation depuis le stockage
Puisque nous avons stocké la délégation signée localement, l'invité peut la récupérer directement depuis 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);
Étape 2 : Créer une exécution
Une exécution représente l'action spécifique que le délégué est autorisé à effectuer sur le compte du delegator. Elle doit respecter tous les caveats définis dans la délégation.
Un objet d'exécution ressemble généralement à ceci :
{
target,
value,
callData
}
Pour simplifier les choses, vous pouvez utiliser la méthode helper createExecution du SDK :
import { createExecution } from "@codefi/delegator-core-viem";
const execution = createExecution(
target,
value,
callData
);
Dans notre cas, puisque nous n'appelons pas un contrat intelligent mais transférons simplement de l'ETH du delegator vers le délégué, l'exécution ressemble à ceci :
const execution = createExecution(
delegateAccount.address,
amount
);
Étape 3 : Soumettre l'exécution avec une UserOperation
Cette partie peut être un peu complexe si vous êtes novice en matière de concepts d'account abstraction, alors décomposons-la.
À un niveau élevé, notre objectif est de soumettre une UserOperation qui encapsule toutes les données de transaction nécessaires pour racheter la délégation. Bien que cela puisse sembler abstrait, cela aboutira finalement à l'exécution d'une transaction Ethereum standard sur la chaîne.
Cette transaction appellera une fonction nommée redeemDelegations sur le Smart Contract Account Delegator. Cette fonction est responsable de :
Vérifier la délégation et valider ses caveats
Déterminer comment l'exécution doit être traitée (mode de rachat). Plus d'informations ici
Exécuter l'action souhaitée (dans ce cas, transférer des tokens)
En termes simples, redeemDelegations vérifie si la délégation est toujours valide et, si tout est en ordre, procède à l'exécution de l'action prévue.
Pour envoyer cette transaction, nous utiliserons la méthode sendUserOperation fournie par le client bundler. Cela abstrait la complexité de la construction et de la signature de la 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,
});