permissionless
Version:
A utility library for working with ERC-4337
317 lines (288 loc) • 9.68 kB
text/typescript
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<UserOperation, "sender"> & {
version: SafeVersion
entryPoint: {
address: Address
version: "0.6" | "0.7"
}
owners: (Account | WebAuthnAccount)[]
account: OneOf<
| EthereumProvider
| WalletClient<Transport, Chain | undefined, Account>
| 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)]
)
}