Constructing a user operation with ERC-7677
Who this guide is for
This guide is for ERC-4337 wallet developers who want to support app-sponsored transactions.
Using Wagmi + permissionless.js
As a prerequisite, your wallet will need to support EIP-5792. This is because the ERC-7677 flow is part of an EIP-5792 wallet_sendCalls
request. Learn more about EIP-5792 here.
Once your wallet supports EIP-5792, you can continue with the rest of this guide, which will focus on the ERC-7677 flow (shown below) from the wallet's perspective.
This guide is specific to EntryPoint v0.6 but the high level flow is the same for v0.7.
1. Construct unsigned user operation for pm_getPaymasterStubData
Before communicating with an app-provided paymaster service, we first need to use the calls provided via a wallet_sendCalls
request to construct a user operation.
import { UserOperation } from "permissionless";
import { useMemo } from "react";
import { Address, Hex, concat, toHex } from "viem";
import { useEstimateMaxPriorityFeePerGas, useGasPrice } from "wagmi";
import {
ENTRYPOINT_ADDRESS_V06_TYPE,
GetEntryPointVersion,
} from "permissionless/types";
type SendCallsParams = {
from: Address;
chainId: Hex;
calls: {
to: Address;
data: Hex;
value: bigint;
}[];
capabilities: {
paymasterService: {
url: string;
context: Record<string, any>;
};
};
};
// Hook that accepts calls and capabilities from an EIP-5792 wallet_sendCalls request.
export function useSponsoredUserOp({
from,
chainId,
calls,
capabilities,
}: SendCallsParams) {
const { data: maxFeePerGas } = useGasPrice();
const { data: maxPriorityFeePerGas } = useEstimateMaxPriorityFeePerGas();
// For illustrative purposes we are just concatenating all the calls' fields together, but in practice the callData
// will depend on the account's implementation.
const callData = useMemo(
() =>
concat(
calls.map((call) => concat([call.to, call.data, toHex(call.value)]))
),
[calls]
);
const userOpPreStubData:
| Omit<
UserOperation<GetEntryPointVersion<ENTRYPOINT_ADDRESS_V06_TYPE>>,
"signature" | "paymasterAndData"
>
| undefined = useMemo(
() =>
maxFeePerGas && maxPriorityFeePerGas
? {
nonce: 0n,
sender: from,
initCode: "0x",
callData,
maxFeePerGas,
maxPriorityFeePerGas,
preVerificationGas: 0n,
verificationGasLimit: 0n,
callGasLimit: 0n,
}
: undefined,
[from, maxFeePerGas, maxPriorityFeePerGas, callData]
);
}
2. Get stub paymaster values for gas estimation
Use permissionless.js to create a paymaster client and call pm_getPaymasterStubData
with the above user operation.
// ...
const userOpPreStubData:
| Omit<
UserOperation<GetEntryPointVersion<ENTRYPOINT_ADDRESS_V06_TYPE>>,
"signature" | "paymasterAndData"
>
| undefined = useMemo(
() =>
maxFeePerGas && maxPriorityFeePerGas
? {
nonce: 0n,
sender: from,
initCode: "0x",
callData,
maxFeePerGas,
maxPriorityFeePerGas,
preVerificationGas: 0n,
verificationGasLimit: 0n,
callGasLimit: 0n,
}
: undefined,
[from, maxFeePerGas, maxPriorityFeePerGas, callData]
);
const { data: paymasterStubData } = usePaymasterStubData({
userOp: userOpPreStubData,
chainId,
paymasterUrl: capabilities.paymasterService.url,
context: capabilities.paymasterService.context,
});
}
3. Estimate user operation gas
Use permissionless.js to create a bundler client and get gas estimates using the user operation we constructed with the paymaster stub values we received from the app-provided paymaster URL.
// ...
const { data: paymasterStubData } = usePaymasterStubData({
userOp: userOpPreStubData,
chainId,
paymasterUrl: capabilities.paymasterService.url,
context: capabilities.paymasterService.context,
});
const userOpWithStubData:
| UserOperation<GetEntryPointVersion<ENTRYPOINT_ADDRESS_V06_TYPE>>
| undefined = useMemo(
() =>
userOpPreStubData && paymasterStubData
? {
...userOpPreStubData,
paymasterAndData: paymasterStubData,
signature: "0x", // Dummy signature, dependent on account implementation.
callGasLimit: 0n,
verificationGasLimit: 0n,
preVerificationGas: 0n,
}
: undefined,
[userOpPreStubData, paymasterStubData]
);
const { data: gasEstimates } = useGasEstimates({
userOp: userOpWithStubData,
chainId,
});
}
4. Get paymaster values
Use permissionless.js to create a paymaster client and call pm_getPaymasterData
to get real paymaster values that will be used during user operation submission (eth_sendUserOperation
).
// ...
const { data: gasEstimates } = useGasEstimates({
userOp: userOpWithStubData,
chainId,
});
const userOpWithGasEstimates = useMemo(
() =>
userOpWithStubData && gasEstimates
? {
...userOpWithStubData,
callGasLimit: gasEstimates.callGasLimit,
verificationGasLimit: gasEstimates.verificationGasLimit,
preVerificationGas: gasEstimates.preVerificationGas,
}
: undefined,
[userOpWithStubData, gasEstimates]
);
const { data: paymasterData } = usePaymasterData({
userOp: userOpWithGasEstimates,
chainId,
paymasterUrl: capabilities.paymasterService.url,
context: capabilities.paymasterService.context,
});
}
5. Construct user operation for user to sign with paymaster values
// ...
const { data: paymasterData } = usePaymasterData({
userOp: userOpWithGasEstimates,
chainId,
paymasterUrl: capabilities.paymasterService.url,
context: capabilities.paymasterService.context,
});
return useMemo(
() => ({
...userOpWithGasEstimates,
paymasterAndData: paymasterData,
}),
[userOpWithGasEstimates, paymasterData]
);
}
This is the user operation the user will sign before the wallet submits it to a bundler using the eth_sendUserOperation
RPC method.