You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
944 lines
28 KiB
944 lines
28 KiB
import * as AbiParameters from '../core/AbiParameters.js'
|
|
import type * as Address from '../core/Address.js'
|
|
import type * as Authorization from '../core/Authorization.js'
|
|
import type * as Errors from '../core/Errors.js'
|
|
import * as Hash from '../core/Hash.js'
|
|
import * as Hex from '../core/Hex.js'
|
|
import type { Assign, Compute, OneOf } from '../core/internal/types.js'
|
|
import * as Signature from '../core/Signature.js'
|
|
import * as TypedData from '../core/TypedData.js'
|
|
import type * as EntryPoint from './EntryPoint.js'
|
|
|
|
/** User Operation. */
|
|
export type UserOperation<
|
|
entryPointVersion extends EntryPoint.Version = EntryPoint.Version,
|
|
signed extends boolean = boolean,
|
|
bigintType = bigint,
|
|
numberType = number,
|
|
> = OneOf<
|
|
| (entryPointVersion extends '0.6' ? V06<signed, bigintType> : never)
|
|
| (entryPointVersion extends '0.7' ? V07<signed, bigintType> : never)
|
|
| (entryPointVersion extends '0.8'
|
|
? V08<signed, bigintType, numberType>
|
|
: never)
|
|
>
|
|
|
|
/**
|
|
* Packed User Operation.
|
|
*
|
|
* @see https://eips.ethereum.org/EIPS/eip-4337#entrypoint-definition
|
|
*/
|
|
export type Packed = {
|
|
/** Concatenation of `verificationGasLimit` (16 bytes) and `callGasLimit` (16 bytes) */
|
|
accountGasLimits: Hex.Hex
|
|
/** The data to pass to the `sender` during the main execution call. */
|
|
callData: Hex.Hex
|
|
/** Concatenation of `factory` and `factoryData`. */
|
|
initCode: Hex.Hex
|
|
/** Concatenation of `maxPriorityFee` (16 bytes) and `maxFeePerGas` (16 bytes) */
|
|
gasFees: Hex.Hex
|
|
/** Anti-replay parameter. */
|
|
nonce: bigint
|
|
/** Concatenation of paymaster fields (or empty). */
|
|
paymasterAndData: Hex.Hex
|
|
/** Extra gas to pay the Bundler. */
|
|
preVerificationGas: bigint
|
|
/** The account making the operation. */
|
|
sender: Address.Address
|
|
/** Data passed into the account to verify authorization. */
|
|
signature: Hex.Hex
|
|
}
|
|
|
|
/** RPC User Operation type. */
|
|
export type Rpc<
|
|
entryPointVersion extends EntryPoint.Version = EntryPoint.Version,
|
|
signed extends boolean = true,
|
|
> = OneOf<
|
|
| (entryPointVersion extends '0.6' ? V06<signed, Hex.Hex> : never)
|
|
| (entryPointVersion extends '0.7' ? V07<signed, Hex.Hex> : never)
|
|
| (entryPointVersion extends '0.8' ? V08<signed, Hex.Hex, Hex.Hex> : never)
|
|
>
|
|
|
|
/** Transaction Info. */
|
|
export type TransactionInfo<
|
|
entryPointVersion extends EntryPoint.Version = EntryPoint.Version,
|
|
bigintType = bigint,
|
|
> = {
|
|
blockHash: Hex.Hex
|
|
blockNumber: bigintType
|
|
entryPoint: Address.Address
|
|
transactionHash: Hex.Hex
|
|
userOperation: UserOperation<entryPointVersion, true, bigintType>
|
|
}
|
|
|
|
/** RPC Transaction Info. */
|
|
export type RpcTransactionInfo<
|
|
entryPointVersion extends EntryPoint.Version = EntryPoint.Version,
|
|
> = TransactionInfo<entryPointVersion, Hex.Hex>
|
|
|
|
/** Type for User Operation on EntryPoint 0.6 */
|
|
export type V06<signed extends boolean = boolean, bigintType = bigint> = {
|
|
/** The data to pass to the `sender` during the main execution call. */
|
|
callData: Hex.Hex
|
|
/** The amount of gas to allocate the main execution call */
|
|
callGasLimit: bigintType
|
|
/** Account init code. Only for new accounts. */
|
|
initCode?: Hex.Hex | undefined
|
|
/** Maximum fee per gas. */
|
|
maxFeePerGas: bigintType
|
|
/** Maximum priority fee per gas. */
|
|
maxPriorityFeePerGas: bigintType
|
|
/** Anti-replay parameter. */
|
|
nonce: bigintType
|
|
/** Paymaster address with calldata. */
|
|
paymasterAndData?: Hex.Hex | undefined
|
|
/** Extra gas to pay the Bundler. */
|
|
preVerificationGas: bigintType
|
|
/** The account making the operation. */
|
|
sender: Address.Address
|
|
/** Data passed into the account to verify authorization. */
|
|
signature?: Hex.Hex | undefined
|
|
/** The amount of gas to allocate for the verification step. */
|
|
verificationGasLimit: bigintType
|
|
} & (signed extends true ? { signature: Hex.Hex } : {})
|
|
|
|
/** RPC User Operation on EntryPoint 0.6 */
|
|
export type RpcV06<signed extends boolean = true> = V06<signed, Hex.Hex>
|
|
|
|
/** Type for User Operation on EntryPoint 0.7 */
|
|
export type V07<signed extends boolean = boolean, bigintType = bigint> = {
|
|
/** The data to pass to the `sender` during the main execution call. */
|
|
callData: Hex.Hex
|
|
/** The amount of gas to allocate the main execution call */
|
|
callGasLimit: bigintType
|
|
/** Account factory. Only for new accounts. */
|
|
factory?: Address.Address | undefined
|
|
/** Data for account factory. */
|
|
factoryData?: Hex.Hex | undefined
|
|
/** Maximum fee per gas. */
|
|
maxFeePerGas: bigintType
|
|
/** Maximum priority fee per gas. */
|
|
maxPriorityFeePerGas: bigintType
|
|
/** Anti-replay parameter. */
|
|
nonce: bigintType
|
|
/** Address of paymaster contract. */
|
|
paymaster?: Address.Address | undefined
|
|
/** Data for paymaster. */
|
|
paymasterData?: Hex.Hex | undefined
|
|
/** The amount of gas to allocate for the paymaster post-operation code. */
|
|
paymasterPostOpGasLimit?: bigintType | undefined
|
|
/** The amount of gas to allocate for the paymaster validation code. */
|
|
paymasterVerificationGasLimit?: bigintType | undefined
|
|
/** Extra gas to pay the Bundler. */
|
|
preVerificationGas: bigintType
|
|
/** The account making the operation. */
|
|
sender: Address.Address
|
|
/** Data passed into the account to verify authorization. */
|
|
signature?: Hex.Hex | undefined
|
|
/** The amount of gas to allocate for the verification step. */
|
|
verificationGasLimit: bigintType
|
|
} & (signed extends true ? { signature: Hex.Hex } : {})
|
|
|
|
/** RPC User Operation on EntryPoint 0.7 */
|
|
export type RpcV07<signed extends boolean = true> = V07<signed, Hex.Hex>
|
|
|
|
/** Type for User Operation on EntryPoint 0.8 */
|
|
export type V08<
|
|
signed extends boolean = boolean,
|
|
bigintType = bigint,
|
|
numberType = number,
|
|
> = {
|
|
/** Authorization data. */
|
|
authorization?: Authorization.Signed<bigintType, numberType> | undefined
|
|
/** The data to pass to the `sender` during the main execution call. */
|
|
callData: Hex.Hex
|
|
/** The amount of gas to allocate the main execution call */
|
|
callGasLimit: bigintType
|
|
/** Account factory. Only for new accounts. */
|
|
factory?: Address.Address | undefined
|
|
/** Data for account factory. */
|
|
factoryData?: Hex.Hex | undefined
|
|
/** Maximum fee per gas. */
|
|
maxFeePerGas: bigintType
|
|
/** Maximum priority fee per gas. */
|
|
maxPriorityFeePerGas: bigintType
|
|
/** Anti-replay parameter. */
|
|
nonce: bigintType
|
|
/** Address of paymaster contract. */
|
|
paymaster?: Address.Address | undefined
|
|
/** Data for paymaster. */
|
|
paymasterData?: Hex.Hex | undefined
|
|
/** The amount of gas to allocate for the paymaster post-operation code. */
|
|
paymasterPostOpGasLimit?: bigintType | undefined
|
|
/** The amount of gas to allocate for the paymaster validation code. */
|
|
paymasterVerificationGasLimit?: bigintType | undefined
|
|
/** Extra gas to pay the Bundler. */
|
|
preVerificationGas: bigintType
|
|
/** The account making the operation. */
|
|
sender: Address.Address
|
|
/** Data passed into the account to verify authorization. */
|
|
signature?: Hex.Hex | undefined
|
|
/** The amount of gas to allocate for the verification step. */
|
|
verificationGasLimit: bigintType
|
|
} & (signed extends true ? { signature: Hex.Hex } : {})
|
|
|
|
/** RPC User Operation on EntryPoint 0.8 */
|
|
export type RpcV08<signed extends boolean = true> = V08<
|
|
signed,
|
|
Hex.Hex,
|
|
Hex.Hex
|
|
>
|
|
|
|
/**
|
|
* Instantiates a {@link ox#UserOperation.UserOperation} from a provided input.
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { Value } from 'ox'
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const userOperation = UserOperation.from({
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: 300_000n,
|
|
* maxFeePerGas: Value.fromGwei('20'),
|
|
* maxPriorityFeePerGas: Value.fromGwei('2'),
|
|
* nonce: 69n,
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* verificationGasLimit: 100_000n,
|
|
* })
|
|
* ```
|
|
*
|
|
* @example
|
|
* ### From Packed User Operation
|
|
*
|
|
* ```ts twoslash
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const packed: UserOperation.Packed = {
|
|
* accountGasLimits: '0x...',
|
|
* callData: '0xdeadbeef',
|
|
* initCode: '0x',
|
|
* gasFees: '0x...',
|
|
* nonce: 69n,
|
|
* paymasterAndData: '0x',
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* signature: '0x',
|
|
* }
|
|
*
|
|
* const userOperation = UserOperation.from(packed)
|
|
* ```
|
|
*
|
|
* @example
|
|
* ### Attaching Signatures
|
|
*
|
|
* ```ts twoslash
|
|
* import { Secp256k1, Value } from 'ox'
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const userOperation = UserOperation.from({
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: 300_000n,
|
|
* maxFeePerGas: Value.fromGwei('20'),
|
|
* maxPriorityFeePerGas: Value.fromGwei('2'),
|
|
* nonce: 69n,
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* verificationGasLimit: 100_000n,
|
|
* })
|
|
*
|
|
* const payload = UserOperation.getSignPayload(userOperation, {
|
|
* chainId: 1,
|
|
* entryPointAddress: '0x1234567890123456789012345678901234567890',
|
|
* entryPointVersion: '0.7',
|
|
* })
|
|
*
|
|
* const signature = Secp256k1.sign({ payload, privateKey: '0x...' })
|
|
*
|
|
* const userOperation_signed = UserOperation.from(userOperation, { signature }) // [!code focus]
|
|
* ```
|
|
*
|
|
* @param userOperation - The user operation to instantiate (structured or packed format).
|
|
* @returns User Operation.
|
|
*/
|
|
export function from<
|
|
const userOperation extends UserOperation | Packed,
|
|
const signature extends Hex.Hex | undefined = undefined,
|
|
>(
|
|
userOperation: userOperation | UserOperation | Packed,
|
|
options: from.Options<signature> = {},
|
|
): from.ReturnType<userOperation, signature> {
|
|
const signature = (() => {
|
|
if (typeof options.signature === 'string') return options.signature
|
|
if (typeof options.signature === 'object')
|
|
return Signature.toHex(options.signature)
|
|
if (userOperation.signature) return userOperation.signature
|
|
return undefined
|
|
})()
|
|
|
|
const packed =
|
|
'accountGasLimits' in userOperation && 'gasFees' in userOperation
|
|
|
|
const userOp = packed ? fromPacked(userOperation) : userOperation
|
|
return { ...userOp, signature } as never
|
|
}
|
|
|
|
export declare namespace from {
|
|
export type Options<
|
|
signature extends Signature.Signature | Hex.Hex | undefined = undefined,
|
|
> = {
|
|
signature?: signature | Signature.Signature | Hex.Hex | undefined
|
|
}
|
|
|
|
export type ReturnType<
|
|
userOperation extends UserOperation | Packed = UserOperation | Packed,
|
|
signature extends Signature.Signature | Hex.Hex | undefined = undefined,
|
|
> = Compute<
|
|
Assign<
|
|
userOperation,
|
|
signature extends Signature.Signature | Hex.Hex
|
|
? Readonly<{ signature: Hex.Hex }>
|
|
: {}
|
|
>
|
|
>
|
|
|
|
export type ErrorType = Errors.GlobalErrorType
|
|
}
|
|
|
|
/**
|
|
* Converts an {@link ox#UserOperation.Rpc} to an {@link ox#UserOperation.UserOperation}.
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const userOperation = UserOperation.fromRpc({
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: '0x69420',
|
|
* maxFeePerGas: '0x2ca6ae494',
|
|
* maxPriorityFeePerGas: '0x41cc3c0',
|
|
* nonce: '0x357',
|
|
* preVerificationGas: '0x69420',
|
|
* signature: '0x',
|
|
* sender: '0x1234567890123456789012345678901234567890',
|
|
* verificationGasLimit: '0x69420',
|
|
* })
|
|
* ```
|
|
*
|
|
* @param rpc - The RPC user operation to convert.
|
|
* @returns An instantiated {@link ox#UserOperation.UserOperation}.
|
|
*/
|
|
export function fromRpc(rpc: Rpc): UserOperation {
|
|
return {
|
|
...rpc,
|
|
callGasLimit: BigInt(rpc.callGasLimit),
|
|
maxFeePerGas: BigInt(rpc.maxFeePerGas),
|
|
maxPriorityFeePerGas: BigInt(rpc.maxPriorityFeePerGas),
|
|
nonce: BigInt(rpc.nonce),
|
|
preVerificationGas: BigInt(rpc.preVerificationGas),
|
|
verificationGasLimit: BigInt(rpc.verificationGasLimit),
|
|
...(rpc.paymasterPostOpGasLimit && {
|
|
paymasterPostOpGasLimit: BigInt(rpc.paymasterPostOpGasLimit),
|
|
}),
|
|
...(rpc.paymasterVerificationGasLimit && {
|
|
paymasterVerificationGasLimit: BigInt(rpc.paymasterVerificationGasLimit),
|
|
}),
|
|
} as UserOperation
|
|
}
|
|
|
|
export declare namespace fromRpc {
|
|
type ErrorType = Errors.GlobalErrorType
|
|
}
|
|
|
|
/**
|
|
* Obtains the signing payload for a {@link ox#UserOperation.UserOperation}.
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { Secp256k1, Value } from 'ox'
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const userOperation = UserOperation.from({
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: 300_000n,
|
|
* maxFeePerGas: Value.fromGwei('20'),
|
|
* maxPriorityFeePerGas: Value.fromGwei('2'),
|
|
* nonce: 69n,
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* verificationGasLimit: 100_000n,
|
|
* })
|
|
*
|
|
* const payload = UserOperation.getSignPayload(userOperation, { // [!code focus]
|
|
* chainId: 1, // [!code focus]
|
|
* entryPointAddress: '0x1234567890123456789012345678901234567890', // [!code focus]
|
|
* entryPointVersion: '0.6', // [!code focus]
|
|
* }) // [!code focus]
|
|
*
|
|
* const signature = Secp256k1.sign({ payload, privateKey: '0x...' })
|
|
* ```
|
|
*
|
|
* @param userOperation - The user operation to get the sign payload for.
|
|
* @returns The signing payload for the user operation.
|
|
*/
|
|
export function getSignPayload<
|
|
entrypointVersion extends EntryPoint.Version = EntryPoint.Version,
|
|
>(
|
|
userOperation: UserOperation<entrypointVersion>,
|
|
options: getSignPayload.Options<entrypointVersion>,
|
|
): Hex.Hex {
|
|
return hash(userOperation, options)
|
|
}
|
|
|
|
export declare namespace getSignPayload {
|
|
type Options<
|
|
entrypointVersion extends EntryPoint.Version = EntryPoint.Version,
|
|
> = hash.Options<entrypointVersion>
|
|
|
|
type ErrorType = hash.ErrorType | Errors.GlobalErrorType
|
|
}
|
|
|
|
/**
|
|
* Hashes a {@link ox#UserOperation.UserOperation}. This is the "user operation hash".
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { Value } from 'ox'
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const userOperation = UserOperation.hash({
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: 300_000n,
|
|
* maxFeePerGas: Value.fromGwei('20'),
|
|
* maxPriorityFeePerGas: Value.fromGwei('2'),
|
|
* nonce: 69n,
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* verificationGasLimit: 100_000n,
|
|
* }, {
|
|
* chainId: 1,
|
|
* entryPointAddress: '0x1234567890123456789012345678901234567890',
|
|
* entryPointVersion: '0.6',
|
|
* })
|
|
* ```
|
|
*
|
|
* @param userOperation - The user operation to hash.
|
|
* @returns The hash of the user operation.
|
|
*/
|
|
export function hash<
|
|
entrypointVersion extends EntryPoint.Version = EntryPoint.Version,
|
|
>(
|
|
userOperation: UserOperation<entrypointVersion>,
|
|
options: hash.Options<entrypointVersion>,
|
|
): Hex.Hex {
|
|
const { chainId, entryPointAddress, entryPointVersion } = options
|
|
const {
|
|
callData,
|
|
callGasLimit,
|
|
initCode,
|
|
factory,
|
|
factoryData,
|
|
maxFeePerGas,
|
|
maxPriorityFeePerGas,
|
|
nonce,
|
|
paymaster,
|
|
paymasterAndData,
|
|
paymasterData,
|
|
paymasterPostOpGasLimit,
|
|
paymasterVerificationGasLimit,
|
|
preVerificationGas,
|
|
sender,
|
|
verificationGasLimit,
|
|
} = userOperation as UserOperation
|
|
|
|
if (entryPointVersion === '0.8') {
|
|
const typedData = toTypedData(userOperation as UserOperation<'0.8', true>, {
|
|
chainId,
|
|
entryPointAddress,
|
|
})
|
|
return TypedData.getSignPayload(typedData)
|
|
}
|
|
|
|
const packedUserOp = (() => {
|
|
if (entryPointVersion === '0.6') {
|
|
return AbiParameters.encode(
|
|
[
|
|
{ type: 'address' },
|
|
{ type: 'uint256' },
|
|
{ type: 'bytes32' },
|
|
{ type: 'bytes32' },
|
|
{ type: 'uint256' },
|
|
{ type: 'uint256' },
|
|
{ type: 'uint256' },
|
|
{ type: 'uint256' },
|
|
{ type: 'uint256' },
|
|
{ type: 'bytes32' },
|
|
],
|
|
[
|
|
sender,
|
|
nonce,
|
|
Hash.keccak256(initCode ?? '0x'),
|
|
Hash.keccak256(callData),
|
|
callGasLimit,
|
|
verificationGasLimit,
|
|
preVerificationGas,
|
|
maxFeePerGas,
|
|
maxPriorityFeePerGas,
|
|
Hash.keccak256(paymasterAndData ?? '0x'),
|
|
],
|
|
)
|
|
}
|
|
|
|
if (entryPointVersion === '0.7') {
|
|
const accountGasLimits = Hex.concat(
|
|
Hex.padLeft(Hex.fromNumber(verificationGasLimit), 16),
|
|
Hex.padLeft(Hex.fromNumber(callGasLimit), 16),
|
|
)
|
|
const gasFees = Hex.concat(
|
|
Hex.padLeft(Hex.fromNumber(maxPriorityFeePerGas), 16),
|
|
Hex.padLeft(Hex.fromNumber(maxFeePerGas), 16),
|
|
)
|
|
const initCode_hashed = Hash.keccak256(
|
|
factory && factoryData ? Hex.concat(factory, factoryData) : '0x',
|
|
)
|
|
const paymasterAndData_hashed = Hash.keccak256(
|
|
paymaster
|
|
? Hex.concat(
|
|
paymaster,
|
|
Hex.padLeft(
|
|
Hex.fromNumber(paymasterVerificationGasLimit || 0),
|
|
16,
|
|
),
|
|
Hex.padLeft(Hex.fromNumber(paymasterPostOpGasLimit || 0), 16),
|
|
paymasterData || '0x',
|
|
)
|
|
: '0x',
|
|
)
|
|
|
|
return AbiParameters.encode(
|
|
[
|
|
{ type: 'address' },
|
|
{ type: 'uint256' },
|
|
{ type: 'bytes32' },
|
|
{ type: 'bytes32' },
|
|
{ type: 'bytes32' },
|
|
{ type: 'uint256' },
|
|
{ type: 'bytes32' },
|
|
{ type: 'bytes32' },
|
|
],
|
|
[
|
|
sender,
|
|
nonce,
|
|
initCode_hashed,
|
|
Hash.keccak256(callData),
|
|
accountGasLimits,
|
|
preVerificationGas,
|
|
gasFees,
|
|
paymasterAndData_hashed,
|
|
],
|
|
)
|
|
}
|
|
|
|
throw new Error(`entryPointVersion "${entryPointVersion}" not supported.`)
|
|
})()
|
|
|
|
return Hash.keccak256(
|
|
AbiParameters.encode(
|
|
[{ type: 'bytes32' }, { type: 'address' }, { type: 'uint256' }],
|
|
[Hash.keccak256(packedUserOp), entryPointAddress, BigInt(chainId)],
|
|
),
|
|
)
|
|
}
|
|
|
|
export declare namespace hash {
|
|
type Options<
|
|
entrypointVersion extends EntryPoint.Version = EntryPoint.Version,
|
|
> = {
|
|
chainId: number
|
|
entryPointAddress: Address.Address
|
|
entryPointVersion: entrypointVersion | EntryPoint.Version
|
|
}
|
|
|
|
type ErrorType =
|
|
| AbiParameters.encode.ErrorType
|
|
| Hash.keccak256.ErrorType
|
|
| Hex.concat.ErrorType
|
|
| Hex.fromNumber.ErrorType
|
|
| Hex.padLeft.ErrorType
|
|
| Errors.GlobalErrorType
|
|
}
|
|
|
|
/**
|
|
* Converts a {@link ox#UserOperation.UserOperation} to `initCode`.
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { Value } from 'ox'
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const initCode = UserOperation.toInitCode({
|
|
* authorization: {
|
|
* address: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* chainId: 1,
|
|
* nonce: 69n,
|
|
* yParity: 0,
|
|
* r: 1n,
|
|
* s: 2n,
|
|
* },
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: 300_000n,
|
|
* factory: '0x7702',
|
|
* factoryData: '0xdeadbeef',
|
|
* maxFeePerGas: Value.fromGwei('20'),
|
|
* maxPriorityFeePerGas: Value.fromGwei('2'),
|
|
* nonce: 69n,
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* })
|
|
* ```
|
|
*
|
|
* @param userOperation - The user operation to convert.
|
|
* @returns The init code.
|
|
*/
|
|
export function toInitCode(userOperation: Partial<UserOperation>): Hex.Hex {
|
|
const { authorization, factory, factoryData } = userOperation
|
|
if (
|
|
factory === '0x7702' ||
|
|
factory === '0x7702000000000000000000000000000000000000'
|
|
) {
|
|
if (!authorization) return '0x7702000000000000000000000000000000000000'
|
|
const delegation = authorization.address
|
|
return Hex.concat(delegation, factoryData ?? '0x')
|
|
}
|
|
if (!factory) return '0x'
|
|
return Hex.concat(factory, factoryData ?? '0x')
|
|
}
|
|
|
|
/**
|
|
* Transforms a User Operation into "packed" format.
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { Value } from 'ox'
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const packed = UserOperation.toPacked({
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: 300_000n,
|
|
* maxFeePerGas: Value.fromGwei('20'),
|
|
* maxPriorityFeePerGas: Value.fromGwei('2'),
|
|
* nonce: 69n,
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* signature: '0x...',
|
|
* verificationGasLimit: 100_000n,
|
|
* })
|
|
* ```
|
|
*
|
|
* @param userOperation - The user operation to transform.
|
|
* @returns The packed user operation.
|
|
*/
|
|
export function toPacked(
|
|
userOperation: UserOperation<'0.7' | '0.8', true>,
|
|
): Packed {
|
|
const {
|
|
callGasLimit,
|
|
callData,
|
|
maxPriorityFeePerGas,
|
|
maxFeePerGas,
|
|
nonce,
|
|
paymaster,
|
|
paymasterData,
|
|
paymasterPostOpGasLimit,
|
|
paymasterVerificationGasLimit,
|
|
sender,
|
|
signature,
|
|
verificationGasLimit,
|
|
} = userOperation
|
|
|
|
const accountGasLimits = Hex.concat(
|
|
Hex.padLeft(Hex.fromNumber(verificationGasLimit || 0n), 16),
|
|
Hex.padLeft(Hex.fromNumber(callGasLimit || 0n), 16),
|
|
)
|
|
const initCode = toInitCode(userOperation)
|
|
const gasFees = Hex.concat(
|
|
Hex.padLeft(Hex.fromNumber(maxPriorityFeePerGas || 0n), 16),
|
|
Hex.padLeft(Hex.fromNumber(maxFeePerGas || 0n), 16),
|
|
)
|
|
const paymasterAndData = paymaster
|
|
? Hex.concat(
|
|
paymaster,
|
|
Hex.padLeft(Hex.fromNumber(paymasterVerificationGasLimit || 0n), 16),
|
|
Hex.padLeft(Hex.fromNumber(paymasterPostOpGasLimit || 0n), 16),
|
|
paymasterData || '0x',
|
|
)
|
|
: '0x'
|
|
const preVerificationGas = userOperation.preVerificationGas ?? 0n
|
|
|
|
return {
|
|
accountGasLimits,
|
|
callData,
|
|
initCode,
|
|
gasFees,
|
|
nonce,
|
|
paymasterAndData,
|
|
preVerificationGas,
|
|
sender,
|
|
signature,
|
|
}
|
|
}
|
|
|
|
export declare namespace toPacked {
|
|
export type ErrorType = Errors.GlobalErrorType
|
|
}
|
|
|
|
/**
|
|
* Transforms a "packed" User Operation into a structured {@link ox#UserOperation.UserOperation}.
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const packed: UserOperation.Packed = {
|
|
* accountGasLimits: '0x...',
|
|
* callData: '0xdeadbeef',
|
|
* initCode: '0x...',
|
|
* gasFees: '0x...',
|
|
* nonce: 69n,
|
|
* paymasterAndData: '0x',
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* signature: '0x...',
|
|
* }
|
|
*
|
|
* const userOperation = UserOperation.fromPacked(packed)
|
|
* ```
|
|
*
|
|
* @param packed - The packed user operation to transform.
|
|
* @returns The structured user operation.
|
|
*/
|
|
export function fromPacked(packed: Packed): UserOperation<'0.7' | '0.8', true> {
|
|
const {
|
|
accountGasLimits,
|
|
callData,
|
|
initCode,
|
|
gasFees,
|
|
nonce,
|
|
paymasterAndData,
|
|
preVerificationGas,
|
|
sender,
|
|
signature,
|
|
} = packed
|
|
|
|
const verificationGasLimit = BigInt(Hex.slice(accountGasLimits, 0, 16))
|
|
const callGasLimit = BigInt(Hex.slice(accountGasLimits, 16, 32))
|
|
|
|
const { factory, factoryData } = (() => {
|
|
if (initCode === '0x') return { factory: undefined, factoryData: undefined }
|
|
|
|
const factory = Hex.slice(initCode, 0, 20)
|
|
const factoryData =
|
|
Hex.size(initCode) > 20 ? Hex.slice(initCode, 20) : undefined
|
|
|
|
return { factory, factoryData }
|
|
})()
|
|
|
|
const maxPriorityFeePerGas = BigInt(Hex.slice(gasFees, 0, 16))
|
|
const maxFeePerGas = BigInt(Hex.slice(gasFees, 16, 32))
|
|
|
|
const {
|
|
paymaster,
|
|
paymasterVerificationGasLimit,
|
|
paymasterPostOpGasLimit,
|
|
paymasterData,
|
|
} = (() => {
|
|
if (paymasterAndData === '0x')
|
|
return {
|
|
paymaster: undefined,
|
|
paymasterVerificationGasLimit: undefined,
|
|
paymasterPostOpGasLimit: undefined,
|
|
paymasterData: undefined,
|
|
}
|
|
|
|
const paymaster = Hex.slice(paymasterAndData, 0, 20)
|
|
const paymasterVerificationGasLimit = BigInt(
|
|
Hex.slice(paymasterAndData, 20, 36),
|
|
)
|
|
const paymasterPostOpGasLimit = BigInt(Hex.slice(paymasterAndData, 36, 52))
|
|
const paymasterData =
|
|
Hex.size(paymasterAndData) > 52
|
|
? Hex.slice(paymasterAndData, 52)
|
|
: undefined
|
|
|
|
return {
|
|
paymaster,
|
|
paymasterVerificationGasLimit,
|
|
paymasterPostOpGasLimit,
|
|
paymasterData,
|
|
}
|
|
})()
|
|
|
|
return {
|
|
callData,
|
|
callGasLimit,
|
|
...(factory && { factory }),
|
|
...(factoryData && { factoryData }),
|
|
maxFeePerGas,
|
|
maxPriorityFeePerGas,
|
|
nonce,
|
|
...(paymaster && { paymaster }),
|
|
...(paymasterData && { paymasterData }),
|
|
...(typeof paymasterPostOpGasLimit === 'bigint' && {
|
|
paymasterPostOpGasLimit,
|
|
}),
|
|
...(typeof paymasterVerificationGasLimit === 'bigint' && {
|
|
paymasterVerificationGasLimit,
|
|
}),
|
|
preVerificationGas,
|
|
sender,
|
|
signature,
|
|
verificationGasLimit,
|
|
}
|
|
}
|
|
|
|
export declare namespace fromPacked {
|
|
export type ErrorType = Hex.slice.ErrorType | Errors.GlobalErrorType
|
|
}
|
|
|
|
/**
|
|
* Converts a {@link ox#UserOperation.UserOperation} to a {@link ox#UserOperation.Rpc}.
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { Value } from 'ox'
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const userOperation = UserOperation.toRpc({
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: 300_000n,
|
|
* maxFeePerGas: Value.fromGwei('20'),
|
|
* maxPriorityFeePerGas: Value.fromGwei('2'),
|
|
* nonce: 69n,
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* verificationGasLimit: 100_000n,
|
|
* })
|
|
* ```
|
|
*
|
|
* @param userOperation - The user operation to convert.
|
|
* @returns An RPC-formatted user operation.
|
|
*/
|
|
export function toRpc(userOperation: UserOperation): Rpc {
|
|
const rpc = {} as Rpc
|
|
|
|
rpc.callData = userOperation.callData
|
|
rpc.callGasLimit = Hex.fromNumber(userOperation.callGasLimit)
|
|
rpc.maxFeePerGas = Hex.fromNumber(userOperation.maxFeePerGas)
|
|
rpc.maxPriorityFeePerGas = Hex.fromNumber(userOperation.maxPriorityFeePerGas)
|
|
rpc.nonce = Hex.fromNumber(userOperation.nonce)
|
|
rpc.preVerificationGas = Hex.fromNumber(userOperation.preVerificationGas)
|
|
rpc.sender = userOperation.sender
|
|
rpc.verificationGasLimit = Hex.fromNumber(userOperation.verificationGasLimit)
|
|
|
|
if (userOperation.factory) rpc.factory = userOperation.factory
|
|
if (userOperation.factoryData) rpc.factoryData = userOperation.factoryData
|
|
if (userOperation.initCode) rpc.initCode = userOperation.initCode
|
|
if (userOperation.paymaster) rpc.paymaster = userOperation.paymaster
|
|
if (userOperation.paymasterData)
|
|
rpc.paymasterData = userOperation.paymasterData
|
|
if (typeof userOperation.paymasterPostOpGasLimit === 'bigint')
|
|
rpc.paymasterPostOpGasLimit = Hex.fromNumber(
|
|
userOperation.paymasterPostOpGasLimit,
|
|
)
|
|
if (typeof userOperation.paymasterVerificationGasLimit === 'bigint')
|
|
rpc.paymasterVerificationGasLimit = Hex.fromNumber(
|
|
userOperation.paymasterVerificationGasLimit,
|
|
)
|
|
if (userOperation.signature) rpc.signature = userOperation.signature
|
|
|
|
return rpc
|
|
}
|
|
|
|
export declare namespace toRpc {
|
|
export type ErrorType = Hex.fromNumber.ErrorType | Errors.GlobalErrorType
|
|
}
|
|
|
|
/**
|
|
* Converts a signed {@link ox#UserOperation.UserOperation} to a {@link ox#TypedData.Definition}.
|
|
*
|
|
* @example
|
|
* ```ts twoslash
|
|
* import { Value } from 'ox'
|
|
* import { UserOperation } from 'ox/erc4337'
|
|
*
|
|
* const typedData = UserOperation.toTypedData({
|
|
* authorization: {
|
|
* chainId: 1,
|
|
* address: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* nonce: 69n,
|
|
* yParity: 0,
|
|
* r: 1n,
|
|
* s: 2n,
|
|
* },
|
|
* callData: '0xdeadbeef',
|
|
* callGasLimit: 300_000n,
|
|
* maxFeePerGas: Value.fromGwei('20'),
|
|
* maxPriorityFeePerGas: Value.fromGwei('2'),
|
|
* nonce: 69n,
|
|
* preVerificationGas: 100_000n,
|
|
* sender: '0x9f1fdab6458c5fc642fa0f4c5af7473c46837357',
|
|
* signature: '0x...',
|
|
* verificationGasLimit: 100_000n,
|
|
* }, {
|
|
* chainId: 1,
|
|
* entryPointAddress: '0x1234567890123456789012345678901234567890',
|
|
* })
|
|
* ```
|
|
*
|
|
* @param userOperation - The user operation to convert.
|
|
* @returns A Typed Data definition.
|
|
*/
|
|
export function toTypedData(
|
|
userOperation: UserOperation<'0.8', true>,
|
|
options: toTypedData.Options,
|
|
): TypedData.Definition<typeof toTypedData.types, 'PackedUserOperation'> {
|
|
const { chainId, entryPointAddress } = options
|
|
|
|
const packedUserOp = toPacked(userOperation)
|
|
|
|
return {
|
|
domain: {
|
|
name: 'ERC4337',
|
|
version: '1',
|
|
chainId,
|
|
verifyingContract: entryPointAddress,
|
|
},
|
|
message: packedUserOp,
|
|
primaryType: 'PackedUserOperation',
|
|
types: toTypedData.types,
|
|
}
|
|
}
|
|
|
|
export namespace toTypedData {
|
|
export type Options = {
|
|
chainId: number
|
|
entryPointAddress: Address.Address
|
|
}
|
|
|
|
export type ErrorType = Errors.GlobalErrorType
|
|
|
|
export const types = {
|
|
PackedUserOperation: [
|
|
{ type: 'address', name: 'sender' },
|
|
{ type: 'uint256', name: 'nonce' },
|
|
{ type: 'bytes', name: 'initCode' },
|
|
{ type: 'bytes', name: 'callData' },
|
|
{ type: 'bytes32', name: 'accountGasLimits' },
|
|
{ type: 'uint256', name: 'preVerificationGas' },
|
|
{ type: 'bytes32', name: 'gasFees' },
|
|
{ type: 'bytes', name: 'paymasterAndData' },
|
|
],
|
|
} as const
|
|
}
|