ox
Version:
Ethereum Standard Library
630 lines • 22.2 kB
JavaScript
import * as AbiParameters from '../core/AbiParameters.js';
import * as Authorization from '../core/Authorization.js';
import * as Hash from '../core/Hash.js';
import * as Hex from '../core/Hex.js';
import * as Quantity from '../core/internal/quantity.js';
import * as Signature from '../core/Signature.js';
import * as TypedData from '../core/TypedData.js';
// ---- Module-scoped constants for hot paths ---------------------------------
/** keccak256 of empty bytes (`0x`). Reused for empty initCode/paymasterAndData. */
const EMPTY_KECCAK = /*#__PURE__*/ Hash.keccak256('0x');
/** Magic suffix for an independently encoded paymaster signature. */
const paymasterSignatureMagic = '0x22e325a297439656';
/** ABI parameters for v0.6 user operation packed encoding. */
const v06HashParameters = [
{ type: 'address' },
{ type: 'uint256' },
{ type: 'bytes32' },
{ type: 'bytes32' },
{ type: 'uint256' },
{ type: 'uint256' },
{ type: 'uint256' },
{ type: 'uint256' },
{ type: 'uint256' },
{ type: 'bytes32' },
];
/** ABI parameters for v0.7 user operation packed encoding. */
const v07HashParameters = [
{ type: 'address' },
{ type: 'uint256' },
{ type: 'bytes32' },
{ type: 'bytes32' },
{ type: 'bytes32' },
{ type: 'uint256' },
{ type: 'bytes32' },
{ type: 'bytes32' },
];
/** ABI parameters for the outer hash envelope `(packedUserOpHash, entryPoint, chainId)`. */
const hashEnvelopeParameters = [
{ type: 'bytes32' },
{ type: 'address' },
{ type: 'uint256' },
];
/**
* Packs two `uint128` values into a single `bytes32` word, with `high` in the
* upper 16 bytes and `low` in the lower 16 bytes. Used for `accountGasLimits`
* and `gasFees` packing in v0.7+ user operations.
*
* @internal
*/
function packUint128Pair(high, low) {
return Hex.concat(Hex.padLeft(Hex.fromNumber(high), 16), Hex.padLeft(Hex.fromNumber(low), 16));
}
/**
* 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(userOperation, options = {}) {
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 };
}
/**
* 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) {
const rpc_ = rpc;
const { eip7702Auth, ...userOperation } = rpc_;
return {
...userOperation,
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),
}),
...(eip7702Auth && {
authorization: Authorization.fromRpc(eip7702Auth),
}),
};
}
/**
* 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(userOperation, options) {
return hash(userOperation, options);
}
/**
* 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(userOperation, options) {
const { chainId, entryPointAddress, entryPointVersion } = options;
const { callData, callGasLimit, initCode, factory, factoryData, maxFeePerGas, maxPriorityFeePerGas, nonce, paymaster, paymasterAndData, paymasterData, paymasterPostOpGasLimit, paymasterVerificationGasLimit, preVerificationGas, sender, verificationGasLimit, } = userOperation;
if (entryPointVersion === '0.8' || entryPointVersion === '0.9') {
const typedData = toTypedData(userOperation, { chainId, entryPointAddress });
return TypedData.getSignPayload(typedData);
}
const packedUserOp = (() => {
if (entryPointVersion === '0.6') {
const initCodeHash = !initCode || initCode === '0x' ? EMPTY_KECCAK : Hash.keccak256(initCode);
const paymasterAndDataHash = !paymasterAndData || paymasterAndData === '0x'
? EMPTY_KECCAK
: Hash.keccak256(paymasterAndData);
return AbiParameters.encode(v06HashParameters, [
sender,
nonce,
initCodeHash,
Hash.keccak256(callData),
callGasLimit,
verificationGasLimit,
preVerificationGas,
maxFeePerGas,
maxPriorityFeePerGas,
paymasterAndDataHash,
]);
}
if (entryPointVersion === '0.7') {
const accountGasLimits = packUint128Pair(verificationGasLimit, callGasLimit);
const gasFees = packUint128Pair(maxPriorityFeePerGas, maxFeePerGas);
const initCode_hashed = factory && factoryData
? Hash.keccak256(Hex.concat(factory, factoryData))
: EMPTY_KECCAK;
const paymasterAndData_hashed = paymaster
? Hash.keccak256(Hex.concat(paymaster, Hex.padLeft(Hex.fromNumber(paymasterVerificationGasLimit || 0), 16), Hex.padLeft(Hex.fromNumber(paymasterPostOpGasLimit || 0), 16), paymasterData || '0x'))
: EMPTY_KECCAK;
return AbiParameters.encode(v07HashParameters, [
sender,
nonce,
initCode_hashed,
Hash.keccak256(callData),
accountGasLimits,
preVerificationGas,
gasFees,
paymasterAndData_hashed,
]);
}
throw new Error(`entryPointVersion "${entryPointVersion}" not supported.`);
})();
return Hash.keccak256(AbiParameters.encode(hashEnvelopeParameters, [
Hash.keccak256(packedUserOp),
entryPointAddress,
BigInt(chainId),
]));
}
/**
* 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: '0x0000000000000000000000000000000000000000000000000000000000000001',
* s: '0x0000000000000000000000000000000000000000000000000000000000000002'
* },
* 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) {
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, options = {}) {
const { callGasLimit, callData, maxPriorityFeePerGas, maxFeePerGas, nonce, paymaster, paymasterData, paymasterPostOpGasLimit, paymasterVerificationGasLimit, sender, signature, verificationGasLimit, } = userOperation;
const { paymasterSignature } = userOperation;
const accountGasLimits = packUint128Pair(verificationGasLimit || 0n, callGasLimit || 0n);
const initCode = toInitCode(userOperation);
const gasFees = packUint128Pair(maxPriorityFeePerGas || 0n, maxFeePerGas || 0n);
const paymasterAndData = paymaster
? Hex.concat(paymaster, Hex.padLeft(Hex.fromNumber(paymasterVerificationGasLimit || 0n), 16), Hex.padLeft(Hex.fromNumber(paymasterPostOpGasLimit || 0n), 16), paymasterData || '0x', ...(paymasterSignature
? options.forHash
? [paymasterSignatureMagic]
: [
paymasterSignature,
Hex.padLeft(Hex.fromNumber(Hex.size(paymasterSignature)), 2),
paymasterSignatureMagic,
]
: []))
: '0x';
const preVerificationGas = userOperation.preVerificationGas ?? 0n;
return {
accountGasLimits,
callData,
initCode,
gasFees,
nonce,
paymasterAndData,
preVerificationGas,
sender,
signature,
};
}
/**
* 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) {
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,
};
}
/**
* 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) {
const userOperation_ = userOperation;
const rpc = {};
rpc.callData = userOperation_.callData;
rpc.callGasLimit = Quantity.fromNumberish(userOperation_.callGasLimit);
rpc.maxFeePerGas = Quantity.fromNumberish(userOperation_.maxFeePerGas);
rpc.maxPriorityFeePerGas = Quantity.fromNumberish(userOperation_.maxPriorityFeePerGas);
rpc.nonce = Quantity.fromNumberish(userOperation_.nonce);
rpc.preVerificationGas = Quantity.fromNumberish(userOperation_.preVerificationGas);
rpc.sender = userOperation_.sender;
rpc.verificationGasLimit = Quantity.fromNumberish(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_.paymasterAndData !== undefined)
rpc.paymasterAndData = userOperation_.paymasterAndData;
if (userOperation_.paymasterData)
rpc.paymasterData = userOperation_.paymasterData;
if (userOperation_.paymasterPostOpGasLimit !== undefined)
rpc.paymasterPostOpGasLimit = Quantity.fromNumberish(userOperation_.paymasterPostOpGasLimit);
if (userOperation_.paymasterVerificationGasLimit !== undefined)
rpc.paymasterVerificationGasLimit = Quantity.fromNumberish(userOperation_.paymasterVerificationGasLimit);
if (userOperation_.paymasterSignature)
rpc.paymasterSignature = userOperation_.paymasterSignature;
if (userOperation_.signature)
rpc.signature = userOperation_.signature;
const authorization = userOperation_
.authorization;
if (authorization)
rpc.eip7702Auth = Authorization.toRpc(authorization);
return rpc;
}
/**
* 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: '0x0000000000000000000000000000000000000000000000000000000000000001',
* s: '0x0000000000000000000000000000000000000000000000000000000000000002'
* },
* 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, options) {
const { chainId, entryPointAddress } = options;
const packedUserOp = toPacked(userOperation, { forHash: true });
return {
domain: {
name: 'ERC4337',
version: '1',
chainId,
verifyingContract: entryPointAddress,
},
message: packedUserOp,
primaryType: 'PackedUserOperation',
types: toTypedData.types,
};
}
(function (toTypedData) {
toTypedData.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' },
],
};
})(toTypedData || (toTypedData = {}));
//# sourceMappingURL=UserOperation.js.map