UNPKG

permissionless

Version:

A utility library for working with ERC-4337

196 lines • 7.95 kB
import { concatHex, decodeAbiParameters, encodeAbiParameters, encodePacked, hashTypedData } from "viem"; import { toOwner } from "../../utils/index.js"; import { getOxExports } from "../../utils/ox.js"; import { EIP712_SAFE_OPERATION_TYPE_V06, EIP712_SAFE_OPERATION_TYPE_V07, getDefaultAddresses, getPaymasterAndData, isWebAuthnAccount } from "./toSafeSmartAccount.js"; export const concatSignatures = (signatures) => { signatures.sort((left, right) => left.signer.toLowerCase().localeCompare(right.signer.toLowerCase())); const SIGNATURE_LENGTH_BYTES = 65; let signatureBytes = "0x"; let dynamicBytes = ""; for (const sig of signatures) { if (sig.dynamic) { /* A contract signature has a static part of 65 bytes and the dynamic part that needs to be appended at the end of signature bytes. The signature format is Signature type == 0 Constant part: 65 bytes {32-bytes signature verifier}{32-bytes dynamic data position}{1-byte signature type} Dynamic part (solidity bytes): 32 bytes + signature data length {32-bytes signature length}{bytes signature data} */ const dynamicPartPosition = (signatures.length * SIGNATURE_LENGTH_BYTES + dynamicBytes.length / 2) .toString(16) .padStart(64, "0"); const dynamicPartLength = (sig.data.slice(2).length / 2) .toString(16) .padStart(64, "0"); const staticSignature = `${sig.signer.slice(2).padStart(64, "0")}${dynamicPartPosition}00`; const dynamicPartWithLength = `${dynamicPartLength}${sig.data.slice(2)}`; signatureBytes += staticSignature; dynamicBytes += dynamicPartWithLength; } else { signatureBytes += sig.data.slice(2); } } signatureBytes += dynamicBytes; return signatureBytes; }; export const getWebAuthnSignature = async ({ owner, hash }) => { const { signature: signatureData, webauthn } = await owner.sign({ hash }); const { Signature } = await getOxExports(); const signature = Signature.fromHex(signatureData); const match = webauthn.clientDataJSON.match(/^\{"type":"webauthn.get","challenge":"[A-Za-z0-9\-_]{43}",(.*)\}$/); if (!match) { throw new Error("challenge not found in client data JSON"); } const [, fields] = match; return encodeAbiParameters([ { name: "authenticatorData", type: "bytes" }, { name: "clientDataJSON", type: "string" }, { name: "signature", type: "uint256[2]" } ], [ webauthn.authenticatorData, fields, [BigInt(signature.r), BigInt(signature.s)] ]); }; export async function signUserOperation(parameters) { const { chainId, entryPoint, validAfter = 0, validUntil = 0, safe4337ModuleAddress: _safe4337ModuleAddress, version, owners, signatures: existingSignatures, account, ...userOperation } = parameters; const { safe4337ModuleAddress } = getDefaultAddresses(version, entryPoint.version, { safe4337ModuleAddress: _safe4337ModuleAddress }); const message = { safe: userOperation.sender, callData: userOperation.callData, nonce: userOperation.nonce, initCode: userOperation.initCode ?? "0x", maxFeePerGas: userOperation.maxFeePerGas, maxPriorityFeePerGas: userOperation.maxPriorityFeePerGas, preVerificationGas: userOperation.preVerificationGas, verificationGasLimit: userOperation.verificationGasLimit, callGasLimit: userOperation.callGasLimit, paymasterAndData: userOperation.paymasterAndData ?? "0x", validAfter: validAfter, validUntil: validUntil, entryPoint: entryPoint.address }; if ("initCode" in userOperation) { message.paymasterAndData = userOperation.paymasterAndData ?? "0x"; } if ("factory" in userOperation) { if (userOperation.factory && userOperation.factoryData) { message.initCode = concatHex([ userOperation.factory, userOperation.factoryData ]); } if (!userOperation.sender) { throw new Error("Sender is required"); } message.paymasterAndData = getPaymasterAndData({ ...userOperation, sender: userOperation.sender }); } const localOwner = isWebAuthnAccount(account) ? account : await toOwner({ owner: account }); const signer = isWebAuthnAccount(localOwner) ? parameters.safeWebAuthnSharedSignerAddress : localOwner.address; if (!signer) { throw new Error("no signer found"); } let unPackedSignatures = []; if (existingSignatures) { try { const decoded = decodeAbiParameters([ { components: [ { type: "address", name: "signer" }, { type: "bytes", name: "data" }, { type: "bool", name: "dynamic" } ], name: "signatures", type: "tuple[]" } ], existingSignatures); unPackedSignatures = decoded[0]; } catch { const decoded = decodeAbiParameters([ { components: [ { type: "address", name: "signer" }, { type: "bytes", name: "data" } ], name: "signatures", type: "tuple[]" } ], existingSignatures); unPackedSignatures = decoded[0].map((sig) => ({ ...sig, dynamic: false })); } } const signatures = [ ...unPackedSignatures, { signer, dynamic: isWebAuthnAccount(localOwner), data: await (async () => { if (isWebAuthnAccount(localOwner)) { const safeHash = hashTypedData({ domain: { chainId, verifyingContract: safe4337ModuleAddress }, types: entryPoint.version === "0.6" ? EIP712_SAFE_OPERATION_TYPE_V06 : EIP712_SAFE_OPERATION_TYPE_V07, primaryType: "SafeOp", message: message }); return getWebAuthnSignature({ owner: localOwner, hash: safeHash }); } return localOwner.signTypedData({ domain: { chainId, verifyingContract: safe4337ModuleAddress }, types: entryPoint.version === "0.6" ? EIP712_SAFE_OPERATION_TYPE_V06 : EIP712_SAFE_OPERATION_TYPE_V07, primaryType: "SafeOp", message: message }); })() } ]; if (signatures.length !== owners.length) { return encodeAbiParameters([ { components: [ { type: "address", name: "signer" }, { type: "bytes", name: "data" }, { type: "bool", name: "dynamic" } ], name: "signatures", type: "tuple[]" } ], [signatures]); } return encodePacked(["uint48", "uint48", "bytes"], [validAfter, validUntil, concatSignatures(signatures)]); } //# sourceMappingURL=signUserOperation.js.map