ERC-4337 Account Abstraction: UserOperation construction, hashing, signing, and bundler JSON-RPC.
Supports both EntryPoint versions, whose wire formats differ:
- v0.6 (
0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789) — the originalUserOperationwith separatecallGasLimit/verificationGasLimitandmaxFeePerGas/maxPriorityFeePerGaswords, plusinitCodeandpaymasterAndDatabyte fields. - v0.7 (
0x0000000071727De22E5E9d8BAf0edAc6f37da032) — thePackedUserOperation:accountGasLimits = verificationGasLimit (high 128) ‖ callGasLimit (low 128)andgasFees = maxPriorityFeePerGas (high 128) ‖ maxFeePerGas (low 128), withinitCode/paymasterAndDataderived from the unpackedfactory*/paymaster*fields. The JSON-RPC representation sent to bundlers stays unpacked (separatefactory,factoryData,paymasterVerificationGasLimit, etc.).
The version is selected per call via the :version option (:v0_6 | :v0_7,
default :v0_7).
userOpHash
Both versions hash as
keccak256(abi.encode(keccak256(packed), entryPoint, chainId)), where packed
is the version-specific abi.encode of the op (variable-length byte fields
hashed with keccak first). This matches EntryPoint.getUserOpHash. Verified
against reference vectors in test/onchain/aa_test.exs (cross-checked with
viem's getUserOperationHash test vectors).
Signing
sign_user_operation/5 signs the userOpHash and returns the op with its
:signature populated. Two schemes:
:eip191(default) — ECDSA overkeccak256("\x19Ethereum Signed Message:\n32" ‖ userOpHash), matching the canonical eth-infinitismSimpleAccount(userOpHash.toEthSignedMessageHash()).:raw— ECDSA over the raw 32-byteuserOpHash(accounts that recover directly without the EIP-191 envelope).
The signature is r ‖ s ‖ v with v ∈ {27, 28}. Accounts with bespoke
signature schemes (Safe, multisig, passkey) should call user_op_hash/4 and
build the signature themselves.
Bundler RPC
send_user_operation/3, estimate_user_operation_gas/3,
get_user_operation_by_hash/2, get_user_operation_receipt/2, and
supported_entry_points/1 wrap the standard bundler methods. The bundler URL
is passed via :bundler_url (or :rpc_url); results are returned raw
(decoded JSON) since shapes are bundler-defined.
Functions
| Function | Purpose |
|---|---|
entry_point/1 | Canonical EntryPoint address for a version |
new/1 | Build + validate a UserOperation from fields |
user_op_hash/4 | Compute the EntryPoint userOpHash |
sign_user_operation/5 | Sign a UserOperation, return it with :signature |
to_rpc_params/2 | Serialize a UserOperation to bundler JSON-RPC params |
send_user_operation/3 | eth_sendUserOperation → userOpHash |
estimate_user_operation_gas/3 | eth_estimateUserOperationGas → gas map |
get_user_operation_by_hash/2 | eth_getUserOperationByHash |
get_user_operation_receipt/2 | eth_getUserOperationReceipt |
supported_entry_points/1 | eth_supportedEntryPoints → addresses |
API Functions
| Function | Arity | Description | Param Kinds |
|---|---|---|---|
supported_entry_points | 1 | List EntryPoint addresses the bundler supports (eth_supportedEntryPoints). | opts: value |
get_user_operation_receipt | 2 | Fetch a UserOperation receipt (eth_getUserOperationReceipt). | user_op_hash: value, opts: value |
get_user_operation_by_hash | 2 | Look up a UserOperation by its hash (eth_getUserOperationByHash). | user_op_hash: value, opts: value |
estimate_user_operation_gas | 3 | Estimate gas for a UserOperation (eth_estimateUserOperationGas). | user_op: value, entry_point: value, opts: value |
send_user_operation | 3 | Submit a UserOperation to a bundler (eth_sendUserOperation). | user_op: value, entry_point: value, opts: value |
to_rpc_params | 2 | Serialize a UserOperation to bundler JSON-RPC params. | user_op: value, opts: value |
sign_user_operation | 5 | Sign a UserOperation and return it with :signature populated. | user_op: value, private_key: value, entry_point: value, chain_id: value, opts: value |
user_op_hash | 4 | Compute the EntryPoint userOpHash for a UserOperation. | user_op: value, entry_point: value, chain_id: value, opts: value |
new | 1 | Build and validate a UserOperation from a map or keyword of fields. | fields: value |
entry_point | 1 | Canonical EntryPoint contract address for a version. | version: value |
Summary
Functions
Canonical EntryPoint contract address for a version.
Estimate gas for a UserOperation (eth_estimateUserOperationGas).
Look up a UserOperation by its hash (eth_getUserOperationByHash).
Fetch a UserOperation receipt (eth_getUserOperationReceipt).
Build and validate a UserOperation from a map or keyword of fields.
Submit a UserOperation to a bundler (eth_sendUserOperation).
Sign a UserOperation and return it with :signature populated.
List EntryPoint addresses the bundler supports (eth_supportedEntryPoints).
Serialize a UserOperation to bundler JSON-RPC params.
Compute the EntryPoint userOpHash for a UserOperation.
Functions
@spec entry_point(:v0_6 | :v0_7) :: String.t()
Canonical EntryPoint contract address for a version.
Parameters
version- EntryPoint version: :v0_6 or :v0_7 (value)
Returns
0x-prefixed checksummed EntryPoint address (string)
# descripex:contract
%{
params: %{
version: %{description: "EntryPoint version: :v0_6 or :v0_7", kind: :value}
},
returns: %{
type: :string,
description: "0x-prefixed checksummed EntryPoint address"
}
}
@spec estimate_user_operation_gas( Onchain.AA.UserOperation.t(), String.t() | binary(), keyword() ) :: {:ok, term()} | {:error, term()}
Estimate gas for a UserOperation (eth_estimateUserOperationGas).
Parameters
user_op- %Onchain.AA.UserOperation{} struct (signature may be a dummy) (value)entry_point- EntryPoint address the bundler should use (value)opts- Options: :version (default :v0_7), :bundler_url (or :rpc_url), :timeout (default:[], value)
Returns
Gas estimate map (e.g. preVerificationGas, verificationGasLimit, callGasLimit) with bundler-defined hex values ({:ok, map} | {:error, term})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :version (default :v0_7), :bundler_url (or :rpc_url), :timeout",
kind: :value
},
entry_point: %{
description: "EntryPoint address the bundler should use",
kind: :value
},
user_op: %{
description: "%Onchain.AA.UserOperation{} struct (signature may be a dummy)",
kind: :value
}
},
returns: %{
type: "{:ok, map} | {:error, term}",
description: "Gas estimate map (e.g. preVerificationGas, verificationGasLimit, callGasLimit) with bundler-defined hex values"
}
}
Look up a UserOperation by its hash (eth_getUserOperationByHash).
Parameters
user_op_hash- 0x-prefixed 32-byte userOpHash (value)opts- Options: :bundler_url (or :rpc_url), :timeout (default:[], value)
Returns
UserOperation + inclusion info map, or nil if the bundler has not seen it ({:ok, map | nil} | {:error, term})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :bundler_url (or :rpc_url), :timeout",
kind: :value
},
user_op_hash: %{description: "0x-prefixed 32-byte userOpHash", kind: :value}
},
returns: %{
type: "{:ok, map | nil} | {:error, term}",
description: "UserOperation + inclusion info map, or nil if the bundler has not seen it"
}
}
Fetch a UserOperation receipt (eth_getUserOperationReceipt).
Parameters
user_op_hash- 0x-prefixed 32-byte userOpHash (value)opts- Options: :bundler_url (or :rpc_url), :timeout (default:[], value)
Returns
Receipt map (success, actualGasUsed, logs, receipt, …), or nil if not yet mined ({:ok, map | nil} | {:error, term})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :bundler_url (or :rpc_url), :timeout",
kind: :value
},
user_op_hash: %{description: "0x-prefixed 32-byte userOpHash", kind: :value}
},
returns: %{
type: "{:ok, map | nil} | {:error, term}",
description: "Receipt map (success, actualGasUsed, logs, receipt, …), or nil if not yet mined"
}
}
@spec new(map() | keyword()) :: {:ok, Onchain.AA.UserOperation.t()} | {:error, term()}
Build and validate a UserOperation from a map or keyword of fields.
Parameters
fields- Map/keyword of UserOperation fields. Required: :sender (address). Numeric fields (:nonce, :call_gas_limit, :verification_gas_limit, :pre_verification_gas, :max_fee_per_gas, :max_priority_fee_per_gas) default to 0. Byte fields (:init_code, :call_data, :paymaster_and_data, :signature) default to "0x". v0.7 unpacked fields :factory, :factory_data, :paymaster, :paymaster_verification_gas_limit, :paymaster_post_op_gas_limit, :paymaster_data default to nil. (value)
Returns
Validated UserOperation struct, or a validation error ({:ok, %Onchain.AA.UserOperation{}} | {:error, term})
# descripex:contract
%{
params: %{
fields: %{
description: "Map/keyword of UserOperation fields. Required: :sender (address). Numeric fields (:nonce, :call_gas_limit, :verification_gas_limit, :pre_verification_gas, :max_fee_per_gas, :max_priority_fee_per_gas) default to 0. Byte fields (:init_code, :call_data, :paymaster_and_data, :signature) default to \"0x\". v0.7 unpacked fields :factory, :factory_data, :paymaster, :paymaster_verification_gas_limit, :paymaster_post_op_gas_limit, :paymaster_data default to nil.",
kind: :value
}
},
returns: %{
type: "{:ok, %Onchain.AA.UserOperation{}} | {:error, term}",
description: "Validated UserOperation struct, or a validation error"
}
}
@spec send_user_operation( Onchain.AA.UserOperation.t(), String.t() | binary(), keyword() ) :: {:ok, term()} | {:error, term()}
Submit a UserOperation to a bundler (eth_sendUserOperation).
Parameters
user_op- Signed %Onchain.AA.UserOperation{} struct (value)entry_point- EntryPoint address the bundler should use (value)opts- Options: :version (default :v0_7), :bundler_url (or :rpc_url), :timeout (default:[], value)
Returns
0x-prefixed userOpHash returned by the bundler ({:ok, String.t()} | {:error, term})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :version (default :v0_7), :bundler_url (or :rpc_url), :timeout",
kind: :value
},
entry_point: %{
description: "EntryPoint address the bundler should use",
kind: :value
},
user_op: %{
description: "Signed %Onchain.AA.UserOperation{} struct",
kind: :value
}
},
returns: %{
type: "{:ok, String.t()} | {:error, term}",
description: "0x-prefixed userOpHash returned by the bundler"
}
}
@spec sign_user_operation( Onchain.AA.UserOperation.t(), binary(), String.t() | binary(), pos_integer(), keyword() ) :: {:ok, Onchain.AA.UserOperation.t()} | {:error, term()}
Sign a UserOperation and return it with :signature populated.
Parameters
user_op- %Onchain.AA.UserOperation{} struct (value)private_key- 32-byte binary or hex private key (with or without 0x) (value)entry_point- EntryPoint address (hex string or 20-byte binary) (value)chain_id- Chain ID integer (value)opts- Options: :version (:v0_6 | :v0_7, default :v0_7), :scheme (:eip191 default, or :raw to sign the userOpHash directly) (default:[], value)
Returns
UserOperation with :signature set to a 65-byte r‖s‖v hex string ({:ok, %Onchain.AA.UserOperation{}} | {:error, term})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :version (:v0_6 | :v0_7, default :v0_7), :scheme (:eip191 default, or :raw to sign the userOpHash directly)",
kind: :value
},
private_key: %{
description: "32-byte binary or hex private key (with or without 0x)",
kind: :value
},
entry_point: %{
description: "EntryPoint address (hex string or 20-byte binary)",
kind: :value
},
user_op: %{description: "%Onchain.AA.UserOperation{} struct", kind: :value},
chain_id: %{description: "Chain ID integer", kind: :value}
},
returns: %{
type: "{:ok, %Onchain.AA.UserOperation{}} | {:error, term}",
description: "UserOperation with :signature set to a 65-byte r‖s‖v hex string"
}
}
List EntryPoint addresses the bundler supports (eth_supportedEntryPoints).
Parameters
opts- Options: :bundler_url (or :rpc_url), :timeout (default:[], value)
Returns
List of 0x-prefixed EntryPoint addresses ({:ok, [String.t()]} | {:error, term})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :bundler_url (or :rpc_url), :timeout",
kind: :value
}
},
returns: %{
type: "{:ok, [String.t()]} | {:error, term}",
description: "List of 0x-prefixed EntryPoint addresses"
}
}
@spec to_rpc_params( Onchain.AA.UserOperation.t(), keyword() ) :: {:ok, map()} | {:error, term()}
Serialize a UserOperation to bundler JSON-RPC params.
Parameters
user_op- %Onchain.AA.UserOperation{} struct (value)opts- Options: :version (:v0_6 | :v0_7, default :v0_7) (default:[], value)
Returns
JSON-RPC UserOperation object with string keys and 0x-quantity numeric fields. v0.7 includes factory/paymaster fields only when set. ({:ok, map} | {:error, term})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :version (:v0_6 | :v0_7, default :v0_7)",
kind: :value
},
user_op: %{description: "%Onchain.AA.UserOperation{} struct", kind: :value}
},
returns: %{
type: "{:ok, map} | {:error, term}",
description: "JSON-RPC UserOperation object with string keys and 0x-quantity numeric fields. v0.7 includes factory/paymaster fields only when set."
}
}
@spec user_op_hash( Onchain.AA.UserOperation.t(), String.t() | binary(), pos_integer(), keyword() ) :: {:ok, String.t()} | {:error, term()}
Compute the EntryPoint userOpHash for a UserOperation.
Parameters
user_op- %Onchain.AA.UserOperation{} struct (value)entry_point- EntryPoint address (hex string or 20-byte binary) (value)chain_id- Chain ID integer (1 = mainnet, 11155111 = Sepolia) (value)opts- Options: :version (:v0_6 | :v0_7, default :v0_7) (default:[], value)
Returns
0x-prefixed 32-byte userOpHash ({:ok, String.t()} | {:error, term})
# descripex:contract
%{
params: %{
opts: %{
default: [],
description: "Options: :version (:v0_6 | :v0_7, default :v0_7)",
kind: :value
},
entry_point: %{
description: "EntryPoint address (hex string or 20-byte binary)",
kind: :value
},
user_op: %{description: "%Onchain.AA.UserOperation{} struct", kind: :value},
chain_id: %{
description: "Chain ID integer (1 = mainnet, 11155111 = Sepolia)",
kind: :value
}
},
returns: %{
type: "{:ok, String.t()} | {:error, term}",
description: "0x-prefixed 32-byte userOpHash",
example: "0x1903d62bb5dc75af6fed866aa46d8e80063d9e288aa7f2caad0ff1fcae22e40d"
}
}