@etherspot/modular-sdk
Version:
Etherspot Modular SDK - build with ERC-7579 smart accounts modules
172 lines • 6.71 kB
JavaScript
import { Buffer } from 'buffer';
import { concat, decodeAbiParameters, encodeAbiParameters, keccak256, pad, parseAbiParameters } from 'viem';
import { hexlifyValue } from './utils/hexlify.js';
import { BigNumber } from '../types/bignumber.js';
// TODO - test this on sepolia
/**
* pack the userOperation
* @param op
* @param forSignature "true" if the hash is needed to calculate the getUserOpHash()
* "false" to pack entire UserOp, for calculating the calldata cost of putting it on-chain.
*/
export function packUserOp(op1, forSignature = true) {
let op;
if ('callGasLimit' in op1) {
op = packUserOpData(op1);
}
else {
op = op1;
}
if (forSignature) {
const packedUserOp = encodeAbiParameters(parseAbiParameters('address, uint256, bytes32, bytes32, bytes32, uint256, bytes32, bytes32'), [op.sender,
BigInt(op.nonce),
keccak256(op.initCode),
keccak256(op.callData),
op.accountGasLimits.toString(),
BigInt(op.preVerificationGas),
op.gasFees.toString(),
keccak256(op.paymasterAndData)]);
return packedUserOp;
}
else {
// for the purpose of calculating gas cost encode also signature (and no keccak of bytes)
const packedUserOp = encodeAbiParameters(parseAbiParameters('address, uint256, bytes, bytes, bytes32, uint256, bytes32, bytes, bytes'), [op.sender,
BigInt(op.nonce),
op.initCode,
op.callData,
op.accountGasLimits.toString(),
BigInt(op.preVerificationGas),
op.gasFees.toString(),
op.paymasterAndData,
op.signature]);
return packedUserOp;
}
}
export function packUint(high128, low128) {
return pad(BigNumber.from(high128).shl(128).add(low128).toHexString(), { size: 32 });
}
// TODO - test this on sepolia
export function packPaymasterData(paymaster, paymasterVerificationGasLimit, postOpGasLimit, paymasterData) {
const paymasterAndData = paymasterData ? paymasterData : '0x';
return concat([
paymaster,
packUint(paymasterVerificationGasLimit, postOpGasLimit),
paymasterAndData
]);
}
// TODO - test this on sepolia
export function packUserOpData(op) {
let paymasterAndData;
if (op.paymaster == null) {
paymasterAndData = '0x';
}
else {
if (op.paymasterVerificationGasLimit == null || op.paymasterPostOpGasLimit == null) {
throw new Error('paymaster with no gas limits');
}
paymasterAndData = packPaymasterData(op.paymaster, op.paymasterVerificationGasLimit, op.paymasterPostOpGasLimit, op.paymasterData);
}
return {
sender: op.sender,
nonce: BigNumber.from(op.nonce).toHexString(),
initCode: op.factory == null ? '0x' : concat([op.factory, op.factoryData ?? '']),
callData: op.callData,
accountGasLimits: packUint(op.verificationGasLimit, op.callGasLimit),
preVerificationGas: BigNumber.from(op.preVerificationGas).toHexString(),
gasFees: packUint(op.maxPriorityFeePerGas, op.maxFeePerGas),
paymasterAndData,
signature: op.signature
};
}
/**
* calculate the userOpHash of a given userOperation.
* The userOpHash is a hash of all UserOperation fields, except the "signature" field.
* The entryPoint uses this value in the emitted UserOperationEvent.
* A wallet may use this value as the hash to sign (the SampleWallet uses this method)
* @param op
* @param entryPoint
* @param chainId
*/
export function getUserOpHash(op, entryPoint, chainId) {
const userOpHash = keccak256(packUserOp(op, true));
//const enc = defaultAbiCoder.encode(['bytes32', 'address', 'uint256'], [userOpHash, entryPoint, chainId]);
const enc = encodeAbiParameters(parseAbiParameters('bytes32, address, uint256'), [userOpHash, entryPoint, BigInt(chainId)]);
return keccak256(enc);
}
const ErrorSig = keccak256(Buffer.from('Error(string)')).slice(0, 10); // 0x08c379a0
const FailedOpSig = keccak256(Buffer.from('FailedOp(uint256,string)')).slice(0, 10); // 0x220266b6
/**
* decode bytes thrown by revert as Error(message) or FailedOp(opIndex,paymaster,message)
*/
// TODO-Test decodeErrorReason
export function decodeErrorReason(error) {
if (error.startsWith(ErrorSig)) {
const [message] = decodeAbiParameters(parseAbiParameters('string'), '0x' + error.substring(10));
return { message };
}
else if (error.startsWith(FailedOpSig)) {
const [opIndexBigInt, message] = decodeAbiParameters(parseAbiParameters('uint256, string'), '0x' + error.substring(10));
const formattedMessage = `FailedOp: ${message}`;
return {
message: formattedMessage,
opIndex: Number(opIndexBigInt),
};
}
}
/**
* update thrown Error object with our custom FailedOp message, and re-throw it.
* updated both "message" and inner encoded "data"
* tested on geth, hardhat-node
* usage: entryPoint.handleOps().catch(decodeError)
*/
export function rethrowError(e) {
let error = e;
let parent = e;
if (error?.error != null) {
error = error.error;
}
while (error?.data != null) {
parent = error;
error = error.data;
}
const decoded = typeof error === 'string' && error.length > 2 ? decodeErrorReason(error) : undefined;
if (decoded != null) {
e.message = decoded.message;
if (decoded.opIndex != null) {
// helper for chai: convert our FailedOp error into "Error(msg)"
const errorWithMsg = concat([ErrorSig, encodeAbiParameters(parseAbiParameters('string'), [decoded.message])]);
// modify in-place the error object:
parent.data = errorWithMsg;
}
}
throw e;
}
/**
* hexlify all members of object, recursively
* @param obj
*/
export function deepHexlify(obj) {
if (typeof obj === 'function') {
return undefined;
}
if (obj == null || typeof obj === 'string' || typeof obj === 'boolean') {
return obj;
}
else if (obj._isBigNumber != null || typeof obj !== 'object') {
const hexlified = hexlifyValue(obj).replace(/^0x0/, '0x');
return hexlified;
}
if (Array.isArray(obj)) {
return obj.map((member) => deepHexlify(member));
}
return Object.keys(obj).reduce((set, key) => ({
...set,
[key]: deepHexlify(obj[key]),
}), {});
}
// resolve all property and hexlify.
// (UserOpMethodHandler receives data from the network, so we need to pack our generated values)
export function resolveHexlify(a) {
return deepHexlify(a);
}
//# sourceMappingURL=ERC4337Utils.js.map