Skip to main content

PackedUserOperation

PackedUserOperation represents an ERC-4337 v0.7+ user operation with optimized field packing. Gas limits and fees are packed into bytes32 fields, reducing calldata size and lowering costs for bundlers.

Quick Start

import { PackedUserOperation } from '@tevm/voltaire/primitives/PackedUserOperation';
import { UserOperation } from '@tevm/voltaire/primitives/UserOperation';
import { ENTRYPOINT_V07 } from '@tevm/voltaire/primitives/EntryPoint';

// Create from v0.6 UserOperation
const userOp = UserOperation.from({
  sender: "0x742d35Cc6634C0532925a3b844Bc9e7595f251e3",
  nonce: 0n,
  initCode: "0x",
  callData: "0x...",
  callGasLimit: 100000n,
  verificationGasLimit: 200000n,
  preVerificationGas: 50000n,
  maxFeePerGas: 1000000000n,
  maxPriorityFeePerGas: 1000000000n,
  paymasterAndData: "0x",
  signature: "0x",
});

// Pack to v0.7
const packed = UserOperation.pack(userOp);

// Hash for signing
const hash = PackedUserOperation.hash(packed, ENTRYPOINT_V07, 1);

// Unpack back to v0.6
const unpacked = PackedUserOperation.unpack(packed);

Type Definition

export type PackedUserOperationType = {
  readonly sender: AddressType;
  readonly nonce: Uint256Type;
  readonly initCode: Uint8Array;
  readonly callData: Uint8Array;
  readonly accountGasLimits: Uint8Array; // bytes32
  readonly preVerificationGas: Uint256Type;
  readonly gasFees: Uint8Array; // bytes32
  readonly paymasterAndData: Uint8Array;
  readonly signature: Uint8Array;
} & { readonly [brand]: "PackedUserOperation" };

Field Packing

accountGasLimits (bytes32)

Packs verification and call gas limits:
verificationGasLimit (128 bits) || callGasLimit (128 bits)

gasFees (bytes32)

Packs priority fee and max fee:
maxPriorityFeePerGas (128 bits) || maxFeePerGas (128 bits)

API Reference

from

Create PackedUserOperation from parameters.
function from(params: {
  sender: AddressType | string | number | bigint | Uint8Array;
  nonce: Uint256Type | bigint | number | string;
  initCode: Uint8Array | string;
  callData: Uint8Array | string;
  accountGasLimits: Uint8Array | string; // 32 bytes
  preVerificationGas: Uint256Type | bigint | number | string;
  gasFees: Uint8Array | string; // 32 bytes
  paymasterAndData: Uint8Array | string;
  signature: Uint8Array | string;
}): PackedUserOperationType

hash

Compute userOpHash for signing.
function hash(
  packedUserOp: PackedUserOperationType,
  entryPoint: AddressType | string | number | bigint | Uint8Array,
  chainId: bigint | number
): Uint8Array

unpack

Convert PackedUserOperation (v0.7) to UserOperation (v0.6).
function unpack(
  packedUserOp: PackedUserOperationType
): UserOperationType

Packing Implementation

Pack Gas Limits

// accountGasLimits = verificationGasLimit (128) || callGasLimit (128)
const accountGasLimits = new Uint8Array(32);

// High 16 bytes: verificationGasLimit
let verification = verificationGasLimit;
for (let i = 15; i >= 0; i--) {
  accountGasLimits[i] = Number(verification & 0xffn);
  verification >>= 8n;
}

// Low 16 bytes: callGasLimit
let call = callGasLimit;
for (let i = 31; i >= 16; i--) {
  accountGasLimits[i] = Number(call & 0xffn);
  call >>= 8n;
}

Pack Gas Fees

// gasFees = maxPriorityFeePerGas (128) || maxFeePerGas (128)
const gasFees = new Uint8Array(32);

// High 16 bytes: maxPriorityFeePerGas
let priority = maxPriorityFeePerGas;
for (let i = 15; i >= 0; i--) {
  gasFees[i] = Number(priority & 0xffn);
  priority >>= 8n;
}

// Low 16 bytes: maxFeePerGas
let max = maxFeePerGas;
for (let i = 31; i >= 16; i--) {
  gasFees[i] = Number(max & 0xffn);
  max >>= 8n;
}

Benefits Over v0.6

  1. Reduced calldata: Smaller operation size
  2. Lower gas costs: Less data to submit
  3. Bundler efficiency: Lower costs for bundlers
  4. Backward compatible: Can convert to/from v0.6

Usage Patterns

Round-trip Conversion

import { UserOperation } from '@tevm/voltaire/primitives/UserOperation';
import { PackedUserOperation } from '@tevm/voltaire/primitives/PackedUserOperation';

// Start with v0.6
const userOp = UserOperation.from({
  sender: "0x...",
  nonce: 0n,
  initCode: "0x",
  callData: "0x...",
  callGasLimit: 100000n,
  verificationGasLimit: 200000n,
  preVerificationGas: 50000n,
  maxFeePerGas: 1500000000n,
  maxPriorityFeePerGas: 1000000000n,
  paymasterAndData: "0x",
  signature: "0x",
});

// Pack to v0.7
const packed = UserOperation.pack(userOp);

// Unpack back to v0.6
const unpacked = PackedUserOperation.unpack(packed);

// Values match
console.log(unpacked.callGasLimit === userOp.callGasLimit); // true
console.log(unpacked.maxFeePerGas === userOp.maxFeePerGas); // true

Direct Creation

import { PackedUserOperation } from '@tevm/voltaire/primitives/PackedUserOperation';

// Manually create packed fields
const accountGasLimits = new Uint8Array(32);
// ... pack verification and call gas

const gasFees = new Uint8Array(32);
// ... pack priority and max fees

const packed = PackedUserOperation.from({
  sender: "0x...",
  nonce: 0n,
  initCode: "0x",
  callData: "0x...",
  accountGasLimits,
  preVerificationGas: 50000n,
  gasFees,
  paymasterAndData: "0x",
  signature: "0x",
});

References