import type { PublicClient } from 'viem'; import GuardianClient from '../guardian'; import { signAndPackTypedData } from './walletHelpers'; import { TypedDataPayload } from './types'; import { JsonRpcError, RpcErrorCode } from './JsonRpcError'; import { RelayerClient } from './relayerClient'; import type { WalletSigner } from '../types'; export type SignTypedDataV4Params = { ethSigner: WalletSigner; rpcProvider: PublicClient; relayerClient: RelayerClient; method: string; params: Array; guardianClient: GuardianClient; }; const REQUIRED_TYPED_DATA_PROPERTIES = ['types', 'domain', 'primaryType', 'message']; const isValidTypedDataPayload = (typedData: object): typedData is TypedDataPayload => ( REQUIRED_TYPED_DATA_PROPERTIES.every((key) => key in typedData) ); export const transformTypedData = (typedData: string | object, chainId: bigint): TypedDataPayload => { let transformedTypedData: object | TypedDataPayload; if (typeof typedData === 'string') { try { transformedTypedData = JSON.parse(typedData); } catch (err: any) { throw new JsonRpcError(RpcErrorCode.INVALID_PARAMS, `Failed to parse typed data JSON: ${err}`); } } else if (typeof typedData === 'object') { transformedTypedData = typedData; } else { throw new JsonRpcError(RpcErrorCode.INVALID_PARAMS, `Invalid typed data argument: ${typedData}`); } if (!isValidTypedDataPayload(transformedTypedData)) { throw new JsonRpcError( RpcErrorCode.INVALID_PARAMS, // eslint-disable-next-line max-len `Invalid typed data argument. The following properties are required: ${REQUIRED_TYPED_DATA_PROPERTIES.join(', ')}`, ); } const providedChainId = transformedTypedData.domain?.chainId; if (providedChainId) { // domain.chainId (if defined) can be a number, string, or hex value. // Normalize to a number for consistent EIP-712 hashing. let normalizedChainId: number; if (typeof providedChainId === 'string') { if (providedChainId.startsWith('0x')) { normalizedChainId = parseInt(providedChainId, 16); } else { normalizedChainId = parseInt(providedChainId, 10); } } else { normalizedChainId = Number(providedChainId); } transformedTypedData.domain.chainId = normalizedChainId; if (BigInt(normalizedChainId) !== chainId) { throw new JsonRpcError(RpcErrorCode.INVALID_PARAMS, `Invalid chainId, expected ${chainId}`); } } return transformedTypedData; }; export const signTypedDataV4 = async ({ params, method, ethSigner, rpcProvider, relayerClient, guardianClient, }: SignTypedDataV4Params): Promise => { const fromAddress: string = params[0]; const typedDataParam: string | object = params[1]; if (!fromAddress || !typedDataParam) { throw new JsonRpcError(RpcErrorCode.INVALID_PARAMS, `${method} requires an address and a typed data JSON`); } const chainId = await rpcProvider.getChainId(); const typedData = transformTypedData(typedDataParam, BigInt(chainId)); await guardianClient.evaluateEIP712Message({ chainID: String(chainId), payload: typedData }); const relayerSignature = await relayerClient.imSignTypedData(fromAddress, typedData); const signature = await signAndPackTypedData( typedData, relayerSignature, BigInt(chainId), fromAddress, ethSigner, ); return signature; };