permissionless
Version:
A utility library for working with ERC-4337
204 lines • 9.94 kB
JavaScript
import { RpcError, encodeFunctionData, erc20Abi, getAddress, maxUint256 } from "viem";
import { getPaymasterData as getPaymasterData_, prepareUserOperation } from "viem/account-abstraction";
import { getChainId as getChainId_ } from "viem/actions";
import { readContract } from "viem/actions";
import { getAction, parseAccount } from "viem/utils";
import { getTokenQuotes } from "../../../actions/pimlico.js";
import { erc20BalanceOverride } from "../../../utils/erc20BalanceOverride.js";
const MAINNET_USDT_ADDRESS = getAddress("0xdAC17F958D2ee523a2206206994597C13D831ec7");
export const prepareUserOperationForErc20Paymaster = (pimlicoClient, { balanceOverride = false, balanceSlot: _balanceSlot } = {}) => async (client, parameters_) => {
const parameters = parameters_;
const account_ = client.account;
if (!account_)
throw new Error("Account not found");
const account = parseAccount(account_);
const bundlerClient = client;
const paymasterContext = parameters.paymasterContext
? parameters.paymasterContext
: bundlerClient?.paymasterContext;
if (typeof paymasterContext === "object" &&
paymasterContext !== null &&
"token" in paymasterContext &&
typeof paymasterContext.token === "string") {
////////////////////////////////////////////////////////////////////////////////
// Inject custom approval before calling prepareUserOperation
////////////////////////////////////////////////////////////////////////////////
const token = getAddress(paymasterContext.token);
let chainId;
async function getChainId() {
if (chainId)
return chainId;
if (client.chain)
return client.chain.id;
const chainId_ = await getAction(client, getChainId_, "getChainId")({});
chainId = chainId_;
return chainId;
}
const quotes = await getAction(pimlicoClient, getTokenQuotes, "getTokenQuotes")({
tokens: [token],
chain: pimlicoClient.chain ?? client.chain ?? account.client.chain,
entryPointAddress: account.entryPoint.address
});
if (quotes.length === 0) {
throw new RpcError(new Error("Quotes not found"), {
shortMessage: "client didn't return token quotes, check if the token is supported"
});
}
const { postOpGas, exchangeRate, paymaster: paymasterERC20Address } = quotes[0];
let calls = parameters.calls;
if (parameters.callData && account.decodeCalls) {
calls = await account.decodeCalls(parameters.callData);
}
// Create basic approval call array with max approval
const callsWithDummyApproval = [
{
abi: erc20Abi,
functionName: "approve",
args: [paymasterERC20Address, maxUint256], // dummy approval to ensure simulation passes
to: paymasterContext.token
},
...(calls ? calls : [])
];
// For USDT on mainnet, add zero approval at the beginning
if (token === MAINNET_USDT_ADDRESS) {
callsWithDummyApproval.unshift({
abi: erc20Abi,
functionName: "approve",
args: [paymasterERC20Address, 0n],
to: MAINNET_USDT_ADDRESS
});
}
////////////////////////////////////////////////////////////////////////////////
// Call prepareUserOperation
////////////////////////////////////////////////////////////////////////////////
const balanceSlot = _balanceSlot ?? quotes[0].balanceSlot;
const hasBalanceSlot = balanceSlot !== undefined;
if (!hasBalanceSlot && balanceOverride) {
throw new Error(`balanceOverride is not supported for token ${token}, provide custom slot for balance & allowance overrides`);
}
const balanceStateOverride = balanceOverride && hasBalanceSlot
? erc20BalanceOverride({
token,
owner: account.address,
slot: balanceSlot
})[0]
: undefined;
parameters.stateOverride =
balanceOverride && balanceStateOverride
? (parameters.stateOverride ?? []).concat([
{
address: token,
stateDiff: [
...(balanceStateOverride.stateDiff ?? [])
]
}
])
: parameters.stateOverride;
const userOperation = await getAction(client, prepareUserOperation, "prepareUserOperation")({
...parameters,
paymaster: {
getPaymasterData: (args) => {
const paymaster = parameters.paymaster ?? bundlerClient?.paymaster;
if (typeof paymaster === "object") {
const { getPaymasterStubData } = paymaster;
if (getPaymasterStubData) {
return getPaymasterStubData(args);
}
}
return getAction(bundlerClient, getPaymasterData_, "getPaymasterData")(args);
}
},
calls: callsWithDummyApproval
});
////////////////////////////////////////////////////////////////////////////////
// Call pimlico_getTokenQuotes and calculate the approval amount needed for op
////////////////////////////////////////////////////////////////////////////////
const maxFeePerGas = userOperation.maxFeePerGas;
const userOperationMaxGas = userOperation.preVerificationGas +
userOperation.callGasLimit +
userOperation.verificationGasLimit +
(userOperation.paymasterPostOpGasLimit || 0n) +
(userOperation.paymasterVerificationGasLimit || 0n);
const userOperationMaxCost = userOperationMaxGas * maxFeePerGas;
// using formula here https://github.com/pimlicolabs/singleton-paymaster/blob/main/src/base/BaseSingletonPaymaster.sol#L334-L341
const maxCostInToken = ((userOperationMaxCost + postOpGas * maxFeePerGas) *
exchangeRate) /
BigInt(1e18);
////////////////////////////////////////////////////////////////////////////////
// Check if we need to approve the token
// If the user has existing approval that is sufficient, skip approval injection
////////////////////////////////////////////////////////////////////////////////
const publicClient = account.client;
const allowance = await getAction(publicClient, readContract, "readContract")({
abi: erc20Abi,
functionName: "allowance",
args: [account.address, paymasterERC20Address],
address: token
});
const hasSufficientApproval = allowance >= maxCostInToken;
const finalCalls = calls ? [...calls] : [];
if (!hasSufficientApproval) {
finalCalls.unshift({
abi: erc20Abi,
functionName: "approve",
args: [paymasterERC20Address, maxCostInToken],
to: paymasterContext.token
});
}
// For USDT on mainnet, add zero approval at the beginning
if (token === MAINNET_USDT_ADDRESS) {
finalCalls.unshift({
abi: erc20Abi,
functionName: "approve",
args: [paymasterERC20Address, 0n],
to: MAINNET_USDT_ADDRESS
});
}
userOperation.callData = await account.encodeCalls(finalCalls.map((call_) => {
const call = call_;
if ("abi" in call)
return {
data: encodeFunctionData(call),
to: call.to,
value: call.value
};
return call;
}));
parameters.calls = finalCalls;
////////////////////////////////////////////////////////////////////////////////
// Declare Paymaster properties. (taken from viem)
////////////////////////////////////////////////////////////////////////////////
const paymaster = parameters.paymaster ?? bundlerClient?.paymaster;
const { getPaymasterData } = (() => {
// If `paymaster: true`, we will assume the Bundler Client supports Paymaster Actions.
if (paymaster === true)
return {
getPaymasterData: (parameters) => getAction(bundlerClient, getPaymasterData_, "getPaymasterData")(parameters)
};
// If Actions are passed to `paymaster` (via Paymaster Client or directly), we will use them.
if (typeof paymaster === "object" &&
paymaster.getPaymasterData) {
const { getPaymasterData } = paymaster;
return {
getPaymasterData
};
}
throw new Error("Expected paymaster: cannot sponsor ERC-20 without paymaster");
})();
////////////////////////////////////////////////////////////////////////////////
// Re-calculate Paymaster data fields.
////////////////////////////////////////////////////////////////////////////////
const paymasterData = await getPaymasterData({
chainId: await getChainId(),
entryPointAddress: account.entryPoint.address,
context: paymasterContext,
...userOperation
});
return {
...userOperation,
...paymasterData
};
}
return (await getAction(client, prepareUserOperation, "prepareUserOperation")(parameters));
};
//# sourceMappingURL=prepareUserOperationForErc20Paymaster.js.map