import { type Account, type Address, type Chain, type Hex, type LocalAccount, type OneOf, type Transport, type UnionPartialBy, type WalletClient, concatHex, decodeAbiParameters, encodeAbiParameters, encodePacked, hashTypedData } from "viem" import type { UserOperation, WebAuthnAccount } from "viem/account-abstraction" import { toOwner } from "../../utils/index.js" import { getOxExports } from "../../utils/ox.js" import type { EthereumProvider } from "../../utils/toOwner.js" import { EIP712_SAFE_OPERATION_TYPE_V06, EIP712_SAFE_OPERATION_TYPE_V07, type SafeVersion, getDefaultAddresses, getPaymasterAndData, isWebAuthnAccount } from "./toSafeSmartAccount.js" export const concatSignatures = ( signatures: { signer: Address; data: Hex; dynamic: boolean }[] ) => { 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 as Hex } export const getWebAuthnSignature = async ({ owner, hash }: { owner: WebAuthnAccount hash: Hex }) => { 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: UnionPartialBy & { version: SafeVersion entryPoint: { address: Address version: "0.6" | "0.7" } owners: (Account | WebAuthnAccount)[] account: OneOf< | EthereumProvider | WalletClient | LocalAccount | WebAuthnAccount > chainId: number signatures?: Hex validAfter?: number validUntil?: number safe4337ModuleAddress?: Address safeWebAuthnSharedSignerAddress?: Address } ) { 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: readonly { signer: Address data: Hex dynamic: boolean }[] = [] 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: { signer: Address; data: Hex; dynamic: boolean }[] = [ ...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)] ) }