Hello Gator quickstart
You can run the hello-gator
quickstart to get started
quickly with the MetaMask Delegation Toolkit.
The quickstart demonstrates creating a delegator account and completing the delegation
lifecycle (creating, signing, and redeeming a delegation).
Prerequisites
Ensure you have access to the private hello-gator
repository.
If you need to gain access, provide the MetaMask Delegation team
with your GitHub username.
Steps
1. Set up the quickstart
Clone the repository:
git clone git@github.com:MetaMask/hello-gator.git
Navigate into the repository and install the dependencies:
cd hello-gator && yarn install
2. Run the quickstart
In the root of the project, run the quickstart:
yarn dev
By default, Hello Gator runs at https://localhost:3000
(it will try other ports if something is already using 3000
).
Navigate to the quickstart example at https://localhost:3000/examples/quickstart
.
The following code shows how the quickstart creates a MetaMaskSmartAccount
and completes the delegation lifecycle.
You can view the project source code on GitHub.
- quickstart.ts
- shared.ts
- page.tsx
import {
type Call,
type DelegationStruct,
type ExecutionStruct,
createCaveatBuilder,
createRootDelegation,
DelegationFramework,
Implementation,
MetaMaskSmartAccount,
SINGLE_DEFAULT_MODE,
toMetaMaskSmartAccount,
} from "@codefi/delegator-core-viem";
import { privateKeyToAccount, generatePrivateKey } from "viem/accounts";
import {
bundlerClient,
createSalt,
publicClient,
getFeePerGas,
} from "../shared";
import { type Address, type Hex, isAddressEqual, zeroAddress } from "viem";
/**
* Create a new MetaMaskSmartAccount representing a Hybrid Delegator Smart
* Account where the signer is a "burner" account.
* @resolves to the MetaMaskSmartAccount instance.
*/
export const createMetaMaskAccount = async () => {
const owner = privateKeyToAccount(generatePrivateKey());
return await toMetaMaskSmartAccount({
client: publicClient,
implementation: Implementation.Hybrid,
deployParams: [owner.address, [], [], []],
deploySalt: createSalt(),
signatory: { account: owner },
});
};
/**
* Create and sign a root delegation, from the delegatorAccount, to the
* delegateAddress, allowing only a transfer of 0 ether to the zero address.
* @param delegatorAccount - The MetaMaskSmartAccount that is creating the delegation.
* @param delegateAddress - The address of the recipient of the delegation.
* @resolves to the signed delegation.
*/
export const createDelegation = async (
delegatorAccount: MetaMaskSmartAccount<Implementation>,
delegateAddress: Address
) => {
// These caveats are allowing only a transfer of 0 ether to the zero address.
// Not a very useful operation, but it demonstrates how caveats that can be
// applied to a delegation.
const caveats = createCaveatBuilder(delegatorAccount.environment)
.addCaveat("allowedTargets", [zeroAddress])
.addCaveat("valueLte", 0n);
const delegation = createRootDelegation(
delegateAddress,
delegatorAccount.address,
caveats,
// The salt is used to create a unique delegation for each call.
BigInt(createSalt())
);
const signature = await delegatorAccount.signDelegation({ delegation });
return {
...delegation,
signature,
};
};
/**
* Redeem the delegation, executing a zero value Call to the zero address. If
* the Delegator is not deployed, a Call will be inserted to deploy the account
* before redeeming the delegation.
* @param redeemerAccount - The MetaMaskSmartAccount redeeming the delegation.
* Must be the `delegate` on the delegation.
* @param delegation - The delegation being redeemed.
* @param delegatorFactoryArgs - The factoryArgs for the delegator account, if
* the account is not deployed.
* @resolves to the UserOperationHash, once it has been settled on chain.
*/
export const executeOnBehalfOfDelegator = async (
redeemerAccount: MetaMaskSmartAccount<Implementation>,
delegation: DelegationStruct,
delegatorFactoryArgs?: { factory: Address; factoryData: Hex }
) => {
if (!isAddressEqual(redeemerAccount.address, delegation.delegate)) {
throw new Error(
`Redeemer account address not equal to delegate. Redeemer: ${redeemerAccount.address}, delegate: ${delegation.delegate}`
);
}
const delegationChain = [delegation];
// The action that the redeemer is executing on behalf of the delegator.
const executions: ExecutionStruct[] = [
{
target: zeroAddress,
value: 0n,
callData: "0x",
},
];
const redeemDelegationCalldata = DelegationFramework.encode.redeemDelegations(
[delegationChain],
[SINGLE_DEFAULT_MODE],
[executions]
);
const calls: Call[] = [
{
to: redeemerAccount.address,
data: redeemDelegationCalldata,
},
];
// If the delegator account is not deployed, it must be deployed before
// redeeming the delegation.
if (delegatorFactoryArgs) {
const { factory, factoryData } = delegatorFactoryArgs;
calls.unshift({
to: factory,
data: factoryData,
});
}
const feePerGas = await getFeePerGas();
const userOperationHash = await bundlerClient.sendUserOperation({
account: redeemerAccount,
calls,
...feePerGas,
});
// This could be in a separate function, for a more responsive user operation,
// but we leave it here for simplicity.
return await bundlerClient.waitForUserOperationReceipt({
hash: userOperationHash,
});
};
import { createPublicClient, toHex, http, Hex } from "viem";
import { randomBytes } from "crypto";
import { sepolia } from "viem/chains";
import {
createBundlerClient,
createPaymasterClient,
} from "viem/account-abstraction";
import { createPimlicoClient } from "permissionless/clients/pimlico";
export const chain = sepolia;
export const BUNDLER_URL = process.env.NEXT_PUBLIC_BUNDLER_URL!;
export const RPC_URL = process.env.NEXT_PUBLIC_RPC_URL!;
export const PAYMASTER_POLICY_ID = process.env.NEXT_PUBLIC_PAYMASTER_POLICY_ID;
export const createSalt = () => toHex(randomBytes(8));
export const publicClient = createPublicClient({
chain,
transport: http(RPC_URL),
});
// todo: add policyId
export const paymasterClient = createPaymasterClient({
transport: http(BUNDLER_URL),
});
export const bundlerClient = createBundlerClient({
transport: http(BUNDLER_URL),
chain,
paymaster: paymasterClient,
});
export const getFeePerGas = async () => {
// The method for determining fee per gas is dependent on the bundler
// implementation. For this reason, this is centralised here.
const pimlicoClient = createPimlicoClient({
chain,
transport: http(BUNDLER_URL),
});
const { fast } = await pimlicoClient.getUserOperationGasPrice();
return fast;
};
// todo: this should be built into the SDK and support non-sepolia chains
export const getExplorerUserOperationLink = (
chainId: number,
userOpHash: Hex
) => `https://jiffyscan.xyz/userOpHash/${userOpHash}?network=sepolia`;
"use client";
import Hero from "@/components/Hero";
import {
createMetaMaskAccount,
createDelegation,
executeOnBehalfOfDelegator,
} from "./quickstart";
import { useState } from "react";
import {
DelegationStruct,
Implementation,
MetaMaskSmartAccount,
getExplorerAddressLink,
getExplorerTransactionLink,
} from "@codefi/delegator-core-viem";
import { chain, getExplorerUserOperationLink } from "../shared";
import { UserOperationReceipt } from "viem/account-abstraction";
function App() {
const [executeOnBehalfIsLoading, setExecuteOnBehalfIsLoading] =
useState(false);
const [delegatorAccount, setDelegatorAccount] =
useState<MetaMaskSmartAccount<Implementation>>();
const [delegateAccount, setDelegateAccount] =
useState<MetaMaskSmartAccount<Implementation>>();
const [delegation, setDelegation] = useState<DelegationStruct>();
const [userOperationReceipt, setUserOperationReceipt] =
useState<UserOperationReceipt>();
const [userOperationErrorMessage, setUserOperationErrorMessage] =
useState<string>();
const [isDelegateDeployed, setIsDelegateDeployed] = useState(false);
const [isDelegatorDeployed, setIsDelegatorDeployed] = useState(false);
const handleCreateDelegator = async () => {
try {
const account = await createMetaMaskAccount();
setDelegatorAccount(account);
} catch (error) {
console.error(error);
}
};
const handleCreateDelegate = async () => {
try {
const account = await createMetaMaskAccount();
setDelegateAccount(account);
} catch (error) {
console.error(error);
}
};
const handleCreateDelegation = async () => {
if (!delegatorAccount || !delegateAccount) return;
// Reset downstream state, as it may be a subsequent delegation.
setDelegation(undefined);
setUserOperationReceipt(undefined);
try {
const delegation = await createDelegation(
delegatorAccount,
delegateAccount.address
);
setDelegation(delegation);
} catch (error) {
console.error(error);
}
};
const handleExecuteOnBehalf = async () => {
if (!delegateAccount || !delegatorAccount || !delegation) return;
setUserOperationReceipt(undefined);
setExecuteOnBehalfIsLoading(true);
const { factory, factoryData } = await delegatorAccount.getFactoryArgs();
const factoryArgs =
factory && factoryData ? { factory, factoryData } : undefined;
try {
const receipt = await executeOnBehalfOfDelegator(
delegateAccount,
delegation,
factoryArgs
);
if (receipt.success) {
setUserOperationReceipt(receipt);
} else {
throw new Error(`User operation failed: ${receipt.reason}`);
}
} catch (error) {
setUserOperationErrorMessage((error as Error).message);
}
setExecuteOnBehalfIsLoading(false);
delegateAccount.isDeployed().then(setIsDelegateDeployed);
delegatorAccount.isDeployed().then(setIsDelegatorDeployed);
};
const handleStartAgain = () => {
setDelegatorAccount(undefined);
setDelegateAccount(undefined);
setDelegation(undefined);
setUserOperationReceipt(undefined);
};
return (
<div className="mx-auto">
<Hero />
<h2 className="text-2xl font-bold mb-4">Quickstart</h2>
<p className="mb-4">
This example demonstrates how to get started with the Delegation
Toolkit. See the{" "}
<a
href="https://docs.gator.metamask.io/get-started/quickstart"
className="text-green-500"
target="_blank"
>
accompanying documentation
</a>
.
</p>
<div className="space-y-4">
<div>
<h3 className="text-lg font-bold">Delegator Account</h3>
<p>
The MetaMask smart contract account that grants authority. Initially
this will be counterfactual (not deployed on-chain), and will be
deployed in the same user operation, just in time for redeeming the
delegation.
</p>
{!delegatorAccount && (
<button
className="bg-white text-black rounded-md px-2 py-1 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-200"
onClick={handleCreateDelegator}
>
Create Delegator Account
</button>
)}
{delegatorAccount && (
<div>
<a
href={getExplorerAddressLink(
chain.id,
delegatorAccount.address
)}
target="_blank"
className="text-green-500 font-mono"
>
{delegatorAccount.address}
</a>{" "}
- {isDelegatorDeployed ? "Deployed" : "Counterfactual"}
</div>
)}
</div>
<div>
<h3 className="text-lg font-bold">Delegate Account</h3>
<p>
The MetaMask smart contract account that receives the{" "}
<span className="font-mono">delegation</span>. Initially this will
be counterfactual (not deployed on-chain), until it is deployed by
submitting a user operation.
</p>
{!delegateAccount && (
<button
className="bg-white text-black rounded-md px-2 py-1 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-200"
onClick={handleCreateDelegate}
>
Create Delegate Account
</button>
)}
{delegateAccount && (
<div>
<a
href={getExplorerAddressLink(chain.id, delegateAccount.address)}
target="_blank"
className="text-green-500 font-mono"
>
{delegateAccount.address}
</a>{" "}
- {isDelegateDeployed ? "Deployed" : "Counterfactual"}
</div>
)}
</div>
<div>
<h3 className="text-lg font-bold">Delegation</h3>
<p>
The <span className="font-mono">delegator</span> creates and signs a{" "}
<span className="font-mono">delegation</span>, granting specific
authority to the <span className="font-mono">delegate account</span>
. In this case, the delegation allows a transfer of 0 ether to the
zero address.
</p>
<button
className="bg-white text-black rounded-md px-2 py-1 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-200"
onClick={handleCreateDelegation}
disabled={
!delegatorAccount || !delegateAccount || executeOnBehalfIsLoading
}
>
Create Delegation
</button>
{delegation && (
<div className="mt-2 p-2 bg-gray-800 rounded">
<pre className="whitespace-pre-wrap break-all">
{JSON.stringify(
delegation,
(_, v) => (typeof v === "bigint" ? `${v.toString()}n` : v),
2
)}
</pre>
</div>
)}
</div>
<div>
<h3 className="text-lg font-bold">Execute</h3>
<p>
The redeemer submits a user operation that executes the action
allowed by the <span className="font-mono">delegation</span> (in
this case, transfer nothing to no one) on behalf of the{" "}
<span className="font-mono">delegator</span>. If the{" "}
<span className="font-mono">delegator</span> is counterfactual, it
will be deployed as a separate{" "}
<span className="font-mono">Call</span> in the same user operation.
</p>
<button
className="bg-white text-black rounded-md px-2 py-1 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-200"
onClick={handleExecuteOnBehalf}
disabled={!delegation || executeOnBehalfIsLoading}
>
Execute
</button>
{executeOnBehalfIsLoading && (
<span className="animate-spin inline-block ml-2">
🐊 loading...
</span>
)}
{userOperationReceipt && (
<div>
User operation hash:{" "}
<a
href={getExplorerUserOperationLink(
chain.id,
userOperationReceipt.userOpHash
)}
className="text-green-500 font-mono"
target="_blank"
>
{userOperationReceipt.userOpHash}
</a>
<br />
Transaction hash:{" "}
<a
href={getExplorerTransactionLink(
chain.id,
userOperationReceipt.receipt.transactionHash
)}
className="text-green-500 font-mono"
target="_blank"
>
{userOperationReceipt.receipt.transactionHash}
</a>
</div>
)}
{userOperationErrorMessage && (
<div className="mt-2 p-2 bg-gray-800 rounded">
<pre className="whitespace-pre-wrap break-all">
Error submitting User Operation: {userOperationErrorMessage}
</pre>
</div>
)}
<div className="mt-4">
<button
onClick={handleStartAgain}
disabled={
(!delegateAccount && !delegatorAccount) ||
executeOnBehalfIsLoading
}
className="bg-white text-black rounded-md px-2 py-1 disabled:opacity-50 disabled:cursor-not-allowed hover:bg-gray-200"
>
Start over
</button>
</div>
</div>
</div>
</div>
);
}
export default App;