En nuestra publicación anterior del blog, ¿Qué es el Delegation Toolkit y qué puedes crear con él?, exploramos cómo el Delegation Toolkit de MetaMask permite a los desarrolladores crear dapps más flexibles y fáciles de usar. Un caso de uso interesante es la creación de una función de invitación social que permita a los usuarios incorporar a sus amigos con la mínima fricción posible.
Este tutorial explica cómo implementar ese tipo de flujo, en el que un usuario existente puede delegar permisos limitados a otro, facilitando que los nuevos usuarios exploren una dapp sin los obstáculos habituales de configurar y financiar una wallet de antemano.
Imagina esto: Alice quiere que Bob pruebe una dapp que ella está usando. Le envía una invitación que le permite reclamar una pequeña cantidad de ETH desde su wallet, por ejemplo, 0,001 ETH, dentro de un límite de tiempo establecido. Bob puede empezar a usar la dapp de inmediato, sin necesidad de instalar una extensión de wallet ni pagar gas fees por su cuenta. Mientras tanto, Alice mantiene el control total de su wallet, sin exponer su clave privada.
Este es el tipo de experiencia de incorporación simplificada que el Delegation Toolkit hace posible, utilizando delegación con permisos y firmas seguras fuera de la cadena. Veamos rápidamente la estructura de lo que vamos a construir.
Invitador
El invitador es el usuario que desea compartir acceso limitado a su wallet para ayudar a incorporar a otra persona. En este tutorial, el invitador utilizará una cuenta MetaMask Delegator, que es una smart contract wallet basada en EIP-4337. Puedes crear una nueva cuenta delegator o usar una existente si ya la has configurado. Para gestionar el despliegue y la infraestructura, utilizaremos los servicios EIP-4337 de Pimlico.
Invitación
Una invitación en este contexto es simplemente una delegación, un objeto firmado creado por la cuenta delegator y almacenado fuera de la cadena. Una vez que la cuenta delegator está desplegada y financiada con ETH, puedes crear una delegación que permita a otro usuario reclamar una determinada cantidad de tokens dentro de un período de tiempo específico. Esta configuración elimina la necesidad de que el invitado financie una wallet o pague gas por adelantado, lo que hace que la incorporación sea mucho más fluida.
Para simplificar las cosas, utilizaremos lo que se denomina una delegación abierta. Esto significa que cualquier persona con acceso a la delegación firmada puede canjearla. Como no está vinculada a un destinatario específico, no es necesario gestionar ni exponer la clave privada del invitador.
Invitado
El invitado es la persona que se está incorporando. Puede utilizar una Externally Owned Account (EOA) o una Smart Contract Account (SCA), según tu implementación. En este tutorial, generaremos automáticamente una EOA para el invitado cuando haga clic en el enlace de reclamación. En una dapp en producción, lo ideal sería detectar si el invitado ya tiene una wallet conectada y utilizarla en su lugar, pero implementar esa lógica está fuera del alcance de esta guía.
Condiciones
Cada invitación puede incluir condiciones para controlar cómo y cuándo puede utilizarse. Estas condiciones, también conocidas como caveats, pueden especificar un límite de financiación, una fecha límite de canje o restricciones sobre cuántas veces puede utilizarse la delegación. En nuestro caso, configuraremos la delegación para que solo pueda canjearse una vez, con una cantidad máxima de tokens (X) y una ventana de tiempo (Y) para garantizar una seguridad básica.
Aceptar la invitación
Cuando el invitado hace clic en el enlace de reclamación y acepta la invitación, está canjeando la delegación. Este proceso activa una user operation en la blockchain para transferir los fondos delegados. Volveremos a utilizar la infraestructura de Pimlico para enviar esta operación y completar el flujo.
Ahora que ya entiendes el flujo, comencemos con la implementación.
Requisitos previos
Antes de empezar a construir, asegúrate de tener lo siguiente configurado:
Node.js (versión 18 o posterior)
Un endpoint RPC de Infura para la testnet Sepolia
Un conocimiento básico de la abstracción de cuentas EIP-4337, incluyendo conceptos clave como Paymaster, Bundler y User Operations
URLs del Bundler y Paymaster de Pimlico
Instalar dependencias
Primero, instala el delegation toolkit:
npm install @metamask/delegation-toolkitA continuación, instala Viem para todas las interacciones con Ethereum:
npm install viemCrear la cuenta del invitador
La cuenta del invitador es la wallet que comparte permisos limitados para ayudar a incorporar a un nuevo usuario. Debe tener suficiente criptomoneda para cubrir los gas fees del invitado durante sus interacciones iniciales con la dapp. En este tutorial, utilizaremos una smart contract account, a menudo denominada cuenta Gator, para desempeñar este papel.
Dicho esto, gracias a la actualización Pectra, las EOAs convencionales con soporte EIP-7702 ahora también pueden delegar permisos. Esto abre la posibilidad de que las wallets tradicionales participen en el intercambio de permisos sin necesidad de cambiar a una smart contract account. Profundizaremos en este enfoque en una publicación aparte.
Para comenzar, crea un archivo llamado index.ts en la raíz de tu proyecto y añade los siguientes fragmentos de código:
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 },
});
Al crear una cuenta delegator, tienes dos opciones principales:
Cuenta híbrida: Este tipo requiere un único firmante, que puede ser una EOA o cualquier firmante compatible con P256. Se especifica usando implementation: Implementation.Hybrid.
Cuenta multisig: Esta opción requiere que múltiples EOAs firmen en nombre de la cuenta delegator. Se especifica usando implementation: Implementation.MultiSig.
Para este tutorial, utilizamos una cuenta híbrida y configuramos el firmante como una EOA convencional. Ten en cuenta que, aunque en el ejemplo anterior generamos una clave privada, esto puede ajustarse para usar la clave privada de una wallet existente de forma programática.
Ahora que hemos creado la cuenta delegator, hay un detalle importante que tener en cuenta: las cuentas creadas con toMetaMaskSmartAccount son contrafácticas. Esto significa que la cuenta no existe en la cadena hasta que se despliega. Puedes seguir obteniendo la dirección de la cuenta usando account.address, pero si intentas buscarla en un explorador de bloques, aún no la encontrarás.
Sin embargo, puedes enviar tokens a esa dirección de antemano. Una vez que la cuenta se despliegue, esos tokens estarán accesibles y cualquier acción pendiente vinculada a esa dirección podrá ejecutarse de inmediato.
Desplegar la cuenta Delegator del invitador
Para avanzar, necesitamos desplegar la cuenta contrafáctica para que pueda interactuar con la blockchain. En la mayoría de las dapps, este paso normalmente se activaría durante la incorporación, por ejemplo, cuando un usuario inicia o crea una sesión.
Para gestionar el despliegue, utilizaremos la infraestructura de Pimlico. Ve a tu panel de Pimlico, crea una clave API y copia la URL proporcionada. Esta URL sirve tanto de bundler (para retransmitir user operations) como de paymaster (para patrocinar gas fees).
Como no existe una función dedicada para desplegar una cuenta contrafáctica, activaremos el despliegue enviando una user operation ficticia al bundler. Esta operación no tendrá éxito, pero Pimlico desplegará la cuenta antes de intentar ejecutarla, que es todo lo que necesitamos.
Configurar el bundler y el paymaster de Pimlico
Instala el paquete @pimlico/permissionless para interactuar con el bundler, el paymaster y las herramientas de user operation de Pimlico:
npm install permissionless A continuación, configuraremos el bundler y el paymaster de Pimlico, y procederemos a desplegar la cuenta 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 });
}
En el fragmento anterior, primero inicializamos los clientes del paymaster y del bundler de Pimlico, seguidos del propio cliente de Pimlico. Luego comprobamos si la cuenta Gator ya ha sido desplegada. Si no lo ha sido, enviamos una user operation ficticia usando la cuenta aún no desplegada como remitente.
El bundler de Pimlico devuelve un hash de transacción una vez que la user operation se agrupa y se envía al mempool. Usamos ese hash para esperar a que la transacción sea minada y confirmada en la cadena.
Para más detalles sobre cómo funciona este proceso internamente, consulta la documentación de Pimlico.
El último paso es financiar tu cuenta delegator recién desplegada con algo de ETH de prueba. Puedes hacerlo visitando el faucet Sepolia de MetaMask.
Crear una invitación
Con una cuenta delegator financiada ya configurada para el invitador, el siguiente paso es crear y firmar una delegación abierta. Esta delegación permitirá al invitado reclamar una cantidad específica de ETH de prueba desde la cuenta del invitador.
Las delegaciones se definen usando el tipo DelegationStruct, que tiene la siguiente estructura:
export type DelegationStruct = {
delegate: Hex;
delegator: Hex;
authority: Hex;
caveats: CaveatStruct[];
salt: bigint;
signature: Hex;
};
En este caso, crearemos una delegación raíz, que es el primer eslabón de una cadena de delegación. Aunque es posible construir sobre una delegación raíz para crear una cadena de re-delegaciones, este tutorial se centrará únicamente en la delegación raíz en sí.
Hay dos formas de crear una delegación raíz:
Si no conoces la dirección del delegado de antemano (como en este tutorial), puedes crear una delegación abierta, que puede ser canjeada por cualquier cuenta.
Si conoces la dirección del delegado, puedes crear una delegación cerrada estableciendo explícitamente el campo delegate con esa dirección.
Optaremos por el enfoque de delegación abierta por su flexibilidad y simplicidad:
...
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,
});
En este paso, hemos añadido tres caveats para controlar cómo puede utilizarse la delegación:
Límite de ejecución: Usando el caveat limitedCalls, restringimos el número de veces que el delegado puede realizar acciones en nombre del invitador.
Límite de transferencia de tokens: El caveat nativeTokenTransferAmount limita la cantidad total de tokens nativos que pueden transferirse desde la cuenta del invitador.
Expiración: Añadimos un caveat basado en marca de tiempo para garantizar que la delegación quede inválida después de un cierto tiempo. En este caso, la invitación expira en 24 horas.
Aunque hay muchos otros caveats que puedes incluir, estos son suficientes para nuestro caso de uso. Puedes encontrar una lista completa de caveats compatibles en la documentación y, si es necesario, también puedes definir caveats personalizados.
Una vez definida la delegación, el último paso es firmarla y almacenarla. Para que una delegación sea válida, debe estar firmada por el delegador. La cuenta delegator de MetaMask expone un método signDelegation, que utilizaremos para este propósito.
En cuanto al almacenamiento, puedes elegir el método que prefieras. En este tutorial, lo mantendremos simple y usaremos localStorage. Ten en cuenta que utilizamos stringify de la biblioteca superjson para serializar la delegación, ya que el JSON.stringify integrado de JavaScript no admite valores BigInt.
npm install superjson...
import { stringify } from "superjson";
const signature = await account.signDelegation({
delegation: openDelegation,
});
localStorage.setItem(
"DELEGATION",
stringify({ delegation: openDelegation, signature })
);
Canjear la invitación
Ahora cambiemos a la perspectiva del invitado, es decir, el usuario que canjeará la delegación y realizará una acción, como transferir tokens desde la cuenta del invitador a la suya propia.
Paso 1: Obtener la delegación del almacenamiento
Como almacenamos la delegación firmada localmente, el invitado puede recuperarla directamente desde 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);
Paso 2: Crear una ejecución
Una ejecución representa la acción específica que el delegado tiene permitido realizar en la cuenta del delegador. Debe respetar todos los caveats definidos en la delegación.
Un objeto de ejecución normalmente tiene este aspecto:
{
target,
value,
callData
}
Para simplificar las cosas, puedes usar el método auxiliar createExecution del SDK:
import { createExecution } from "@codefi/delegator-core-viem";
const execution = createExecution(
target,
value,
callData
);
En nuestro caso, como no estamos llamando a un smart contract sino simplemente transfiriendo ETH del delegador al delegado, la ejecución tiene este aspecto:
const execution = createExecution(
delegateAccount.address,
amount
);
Paso 3: Enviar la ejecución con una UserOperation
Esta parte puede resultar algo compleja si eres nuevo en los conceptos de abstracción de cuentas, así que vamos a desglosarla.
A grandes rasgos, nuestro objetivo es enviar una UserOperation que encapsule todos los datos de transacción necesarios para canjear la delegación. Aunque pueda parecer abstracto, esto resultará en última instancia en una transacción estándar de Ethereum ejecutada en la cadena.
Esta transacción llamará a una función denominada redeemDelegations en la Delegator Smart Contract Account. Esta función es responsable de:
Verificar la delegación y validar sus caveats
Determinar cómo debe procesarse la ejecución (modo de canje). Más información aquí
Ejecutar la acción deseada (en este caso, transferir tokens)
En términos simples, redeemDelegations comprueba si la delegación sigue siendo válida y, si todo está en orden, procede a realizar la acción prevista.
Para enviar esta transacción, utilizaremos el método sendUserOperation proporcionado por el cliente bundler. Esto abstrae la complejidad de construir y firmar 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,
});