diff --git a/la-utils/la-transactions/handlers/signEip7702Auth.ts b/la-utils/la-transactions/handlers/signEip7702Auth.ts new file mode 100644 index 0000000..93e1109 --- /dev/null +++ b/la-utils/la-transactions/handlers/signEip7702Auth.ts @@ -0,0 +1,51 @@ +import { signAuthorization } from "../primitive/signAuthorization"; +import { toEthAddress } from "../../la-pkp/toEthAddress"; + +/** + * Handler function for EIP-7702 authorization signing using PKP + * This function provides a high-level interface for generating EIP-7702 compliant authorization tuples using PKP + * + * @param {Object} params - The parameters object + * @param {string} params.pkpPublicKey - The PKP's public key + * @param {string} params.targetAddress - The address being authorized + * @param {string|number} params.chainId - The chain ID (0 for universal deployment) + * @param {string|number} params.nonce - Optional nonce value (defaults to 0 for new accounts) + * @returns {Promise<{ + * chainId: number, + * address: string, + * nonce: number, + * yParity: number, + * r: string, + * s: string, + * signer: string + * }>} The authorization tuple components and signer address + */ +export const signEip7702Auth = async ({ + pkpPublicKey, + targetAddress, + chainId = 0, + nonce = 0, +}: { + pkpPublicKey: string; + targetAddress: string; + chainId?: string | number; + nonce?: string | number; +}) => { + // Get the signer's address from PKP + const signerAddress = toEthAddress(pkpPublicKey); + + // Generate the authorization tuple + const authTuple = await signAuthorization({ + sigName: "eip7702-auth", + pkpPublicKey, + chainId, + target: targetAddress, + nonce, + }); + + // Return authorization tuple with signer address + return { + ...authTuple, + signer: signerAddress, + }; +}; diff --git a/la-utils/la-transactions/handlers/signEip7702AuthViem.ts b/la-utils/la-transactions/handlers/signEip7702AuthViem.ts new file mode 100644 index 0000000..06d8521 --- /dev/null +++ b/la-utils/la-transactions/handlers/signEip7702AuthViem.ts @@ -0,0 +1,68 @@ +import { concatHex, encodeAbiParameters, type Hex } from "viem"; +import { privateKeyToAccount } from "viem/accounts"; + +/** + * Signs an EIP-7702 authorization using a private key via Viem + * This function provides a direct interface to sign EIP-7702 authorizations using Viem's primitives and a private key + * + * @param {Object} params - The parameters object + * @param {Hex} params.privateKey - The private key to sign with (must be in 0x format) + * @param {string} params.targetAddress - The address being authorized + * @param {bigint} params.chainId - The chain ID (defaults to 1n for Ethereum mainnet) + * @param {bigint} params.nonce - The nonce value (defaults to 0n) + * @returns {Promise<{ + * chainId: number, + * address: string, + * nonce: number, + * yParity: number, + * r: string, + * s: string, + * signer: string + * }>} The authorization tuple components and signer address + */ +export const signEip7702AuthViem = async ({ + privateKey, + targetAddress, + chainId = 1n, + nonce = 0n, +}: { + privateKey: Hex; + targetAddress: string; + chainId?: bigint; + nonce?: bigint; +}) => { + const wallet = privateKeyToAccount(privateKey); + + const message = { + chainId, + target: targetAddress as Hex, + nonce, + }; + + // Sign using Viem's signMessage with EIP-7702 format + const signature = await wallet.signMessage({ + message: concatHex([ + "0x05", // MAGIC prefix for EIP-7702 + encodeAbiParameters( + [{ type: "uint256" }, { type: "address" }, { type: "uint64" }], + [message.chainId, message.target, message.nonce] + ), + ]), + }); + + // Extract r, s, v components from the signature + const r = `0x${signature.slice(2, 66)}`; + const s = `0x${signature.slice(66, 130)}`; + const v = parseInt(signature.slice(130, 132), 16); + + // Return in the same format as PKP auth tuple + return { + chainId: Number(chainId), + address: targetAddress, + nonce: Number(nonce), + yParity: v - 27, // Convert v to yParity + r, + s, + signer: wallet.address + }; +}; \ No newline at end of file diff --git a/la-utils/la-transactions/primitive/signAuthorization.ts b/la-utils/la-transactions/primitive/signAuthorization.ts new file mode 100644 index 0000000..a1c3c95 --- /dev/null +++ b/la-utils/la-transactions/primitive/signAuthorization.ts @@ -0,0 +1,210 @@ +// Validate inputs according to EIP-7702 spec +/** Maximum value for 256-bit unsigned integers (2^256 - 1) + * Used to validate: + * - signature components (r,s) + * - chainId + */ +const MAX_UINT256 = BigInt( + "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" +); + +/** Maximum value for 64-bit unsigned integers (2^64 - 1) + * Used to validate: + * - nonce value + */ +const MAX_UINT64 = BigInt("0xffffffffffffffff"); + +/** Maximum value for 8-bit unsigned integers (2^8 - 1) + * Used to validate: + * - signature yParity (v) value + */ +const MAX_UINT8 = BigInt("0xff"); + +/** + * Signs an EIP-7702 authorization tuple + * This function implements the authorization signing as specified in EIP-7702 + * + * @param {Object} params - The parameters object + * @param {string} params.pkpPublicKey - The PKP's public key + * @param {string|number} params.chainId - The chain ID (0 for universal deployment) + * @param {string} params.target - The target address that will be authorized + * @param {string|number} params.nonce - The nonce value (must be < 2^64) + * @param {string} params.sigName - The name of the signature for tracking + * @returns {Promise<{chainId: number, address: string, nonce: number, yParity: number, r: string, s: string}>} + * The authorization tuple components as specified in EIP-7702 + */ +export const signAuthorization = async ({ + sigName, + pkpPublicKey, + chainId, + target, + nonce, +}: { + sigName: string; + pkpPublicKey: string; + chainId: string | number; + target: string; + nonce: string | number; +}) => { + console.log("=== Debug: Input Parameters ==="); + console.log("pkpPublicKey:", pkpPublicKey); + console.log("chainId:", chainId); + console.log("target:", target); + console.log("nonce:", nonce); + + console.log("\n=== Debug: Max Values ==="); + console.log("MAX_UINT256 (hex):", MAX_UINT256.toString(16)); + console.log("MAX_UINT64 (hex):", MAX_UINT64.toString(16)); + console.log("MAX_UINT8 (hex):", MAX_UINT8.toString(16)); + + // Validate chainId + const numericChainId = BigInt(chainId); + console.log("numericChainId:", numericChainId); + if (numericChainId > MAX_UINT256) { + throw new Error("Chain ID must be less than 2^256"); + } + + // Validate nonce + const numericNonce = BigInt(nonce); + console.log("numericNonce:", numericNonce); + if (numericNonce > MAX_UINT64) { + throw new Error("Nonce must be less than 2^64"); + } + + // Validate target address + if (!target.startsWith("0x")) { + throw new Error("Target address must start with 0x"); + } + const addressWithoutPrefix = target.slice(2); + console.log("addressWithoutPrefix:", addressWithoutPrefix); + if (addressWithoutPrefix.length !== 40) { + // 20 bytes = 40 hex chars + throw new Error("Target address must be 20 bytes long"); + } + if (!/^[0-9a-fA-F]+$/.test(addressWithoutPrefix)) { + throw new Error("Target address must be a valid hex string"); + } + + // Format the PKP public key if needed + const pkForLit = pkpPublicKey.startsWith("0x") + ? pkpPublicKey.slice(2) + : pkpPublicKey; + + // Create the authorization message according to EIP-7702 spec + // MAGIC (0x05) || rlp([chain_id, address, nonce]) + const MAGIC = "0x05"; + const rlpEncoded = ethers.utils.RLP.encode([ + ethers.utils.hexlify(chainId), + target, + ethers.utils.hexlify(nonce), + ]); + + console.log("\n=== Debug: Message Construction ==="); + console.log("MAGIC:", MAGIC); + console.log("RLP encoded:", rlpEncoded); + + const messageToSign = ethers.utils.concat([ + ethers.utils.hexlify(MAGIC), + rlpEncoded, + ]); + + console.log("Message to sign:", messageToSign); + + // Hash the message + const messageHash = ethers.utils.keccak256(messageToSign); + console.log("Message hash:", messageHash); + + // Sign the hash using PKP + const sig = await Lit.Actions.signAndCombineEcdsa({ + toSign: ethers.utils.arrayify(messageHash), + publicKey: pkForLit, + sigName, + }); + + console.log("\n=== Debug: Raw Signature ==="); + console.log("Raw signature:", sig); + + // Parse signature components + const parsedSig = JSON.parse(sig); + console.log("\n=== Debug: Parsed Signature Components ==="); + console.log("v:", parsedSig.v); + console.log("r:", parsedSig.r); + console.log("s:", parsedSig.s); + + // Validate signature components + const yParity = BigInt(parsedSig.v); + console.log("\n=== Debug: yParity Validation ==="); + console.log("yParity:", yParity.toString()); + console.log("MAX_UINT8:", MAX_UINT8.toString(16)); + + if (yParity > MAX_UINT8) { + throw new Error("y_parity must be less than 2^8"); + } + + // Add hex prefix for BigInt conversion and ensure exactly 64 characters (32 bytes) + const rHexRaw = parsedSig.r.replace(/^0+/, ""); + const sHexRaw = parsedSig.s.replace(/^0+/, ""); + + // Take the last 64 characters to ensure correct length + const rHex = "0x" + rHexRaw.slice(-64); + const sHex = "0x" + sHexRaw.slice(-64); + + console.log("\n=== Debug: r/s Hex Values ==="); + console.log("Original r:", parsedSig.r); + console.log("Truncated rHex:", rHex); + console.log("Original s:", parsedSig.s); + console.log("Truncated sHex:", sHex); + + try { + const r = BigInt(rHex); + console.log("\n=== Debug: r Value Detailed Comparison ==="); + console.log("r decimal:", r.toString()); + console.log("r hex:", r.toString(16)); + console.log("r length in hex:", r.toString(16).length); + console.log("MAX_UINT256 decimal:", MAX_UINT256.toString()); + console.log("MAX_UINT256 hex:", MAX_UINT256.toString(16)); + console.log("MAX_UINT256 length in hex:", MAX_UINT256.toString(16).length); + console.log("Is r > MAX_UINT256?", r > MAX_UINT256); + console.log("Difference:", (r - MAX_UINT256).toString()); + + if (r > MAX_UINT256) { + throw new Error("r must be less than 2^256"); + } + + const s = BigInt(sHex); + console.log("\n=== Debug: s Value ==="); + console.log("s decimal:", s.toString()); + console.log("s hex:", s.toString(16)); + + if (s > MAX_UINT256) { + throw new Error("s must be less than 2^256"); + } + + // Additional EIP-2 validation for s value + const secp256k1n = BigInt( + "0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141" + ); + console.log("\n=== Debug: secp256k1n Validation ==="); + console.log("secp256k1n/2:", (secp256k1n / BigInt(2)).toString(16)); + + if (s > secp256k1n / BigInt(2)) { + throw new Error( + "s value must be less than or equal to secp256k1n/2 as per EIP-2" + ); + } + + // Return the authorization tuple components + return { + chainId: Number(chainId), + address: target, + nonce: Number(nonce), + yParity: Number(yParity), + r: rHex, + s: sHex, + }; + } catch (error) { + console.error("\n=== Debug: Error Details ==="); + console.error("Error converting or validating r/s values:", error); + throw error; + } +}; diff --git a/my-app/main.ts b/my-app/main.ts index 701c6a1..1c35e1e 100644 --- a/my-app/main.ts +++ b/my-app/main.ts @@ -4,12 +4,15 @@ import { readPKP } from "../scripts/utils/io"; (async () => { const lit = await createLitService(); + const pkp = readPKP(); - console.log("🏃‍♂️ Running Lit Action..."); + console.log("🏃‍♂️ Running EIP-7702 Authorization Example..."); const res = await lit.executeJs({ code: litActionCodeString, params: { - pkpPublicKey: readPKP().pkpInfo.publicKey, + pkpPublicKey: pkp.pkpInfo.publicKey, + targetAddress: "0x1234567890123456789012345678901234567890", // Example target address + chainId: 0, // 0 for universal deployment }, }); diff --git a/my-lit-action/lit-action.ts b/my-lit-action/lit-action.ts index b512184..9edbb67 100644 --- a/my-lit-action/lit-action.ts +++ b/my-lit-action/lit-action.ts @@ -2,8 +2,11 @@ import { getYellowstoneProvider } from "../la-utils/la-chain/yellowstone/getYell import { toEthAddress } from "../la-utils/la-pkp/toEthAddress"; import { contractCall } from "../la-utils/la-transactions/handlers/contractCall"; import { nativeSend } from "../la-utils/la-transactions/handlers/nativeSend"; +import { signEip7702Auth } from "../la-utils/la-transactions/handlers/signEip7702Auth"; +import { signEip7702AuthViem } from "../la-utils/la-transactions/handlers/signEip7702AuthViem"; import { contractExample } from "./contract-example"; import { composeTxUrl } from "./utils"; +import { type Hex } from "viem"; // Define your jsParams here. It's sending from ./my-app/main.ts declare global { @@ -12,49 +15,40 @@ declare global { }; } +const TARGET_ADDRESS = "0x341E5273E2E2ea3c4aDa4101F008b1261E58510D"; + (async () => { - console.log("👋 Hello via Lit Action!"); - - // Access your jsParams here - console.log("PKP Public Key:", params.pkpPublicKey); - - // using a helper function - const pkpEthAddress = toEthAddress(params.pkpPublicKey); - console.log("PKP ETH Address:", pkpEthAddress); - - // Get the provider - const provider = await getYellowstoneProvider(); - - // Example 1: Send transaction using the nativeSend handler, which is a wrapper around the primitive functions - const txHash = await nativeSend({ - provider, - pkpPublicKey: params.pkpPublicKey, - to: pkpEthAddress, - amount: "0.0001", - }); - - // Example 2: Call a contract function - const txHash2 = await contractCall({ - provider, - pkpPublicKey: params.pkpPublicKey, - callerAddress: pkpEthAddress, - abi: [contractExample.methods.mintNextAndAddAuthMethods], - contractAddress: contractExample.address, - functionName: "mintNextAndAddAuthMethods", - args: [ - 2, - [2], - ["0x170d13600caea2933912f39a0334eca3d22e472be203f937c4bad0213d92ed71"], - ["0x0000000000000000000000000000000000000000000000000000000000000000"], - [[1]], - true, - true, - ], - overrides: { - value: 1n, - }, - }); - - console.log(`🎉 [nativeSend] Transaction sent: ${composeTxUrl(txHash)}`); - console.log(`🎉 [contractCall] Transaction sent: ${composeTxUrl(txHash2)}`); + console.log("🔐 EIP-7702 Authorization Examples"); + + // Example 1: Using PKP to sign EIP-7702 authorization + try { + // Generate the EIP-7702 authorization tuple using PKP + const authTuple = await signEip7702Auth({ + pkpPublicKey: params.pkpPublicKey, + targetAddress: TARGET_ADDRESS, + chainId: 1, + }); + + console.log("✅ PKP Authorization tuple generated successfully!"); + console.log("authTuple:", authTuple); + } catch (error) { + console.error("❌ Failed to sign PKP authorization:", error); + } + + // Example 2: Using Viem with private key to sign EIP-7702 authorization + try { + const privateKey = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex; // Example private key + + const signature = await signEip7702AuthViem({ + privateKey, + targetAddress: TARGET_ADDRESS, + chainId: 1n, + nonce: 0n, + }); + + console.log("✅ Private key Authorization tuple generated:", signature); + } catch (error) { + console.error("❌ Failed to sign with Viem:", error); + } })(); diff --git a/package.json b/package.json index 9d51faa..80cd686 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "inquirer": "^12.4.2", "json-with-bigint": "^2.4.2", "pino-caller": "^3.4.0", + "viem": "^2.23.5", "zod": "^3.24.2" } } diff --git a/scripts/postbuild.ts b/scripts/postbuild.ts index 7a78291..419e36f 100644 --- a/scripts/postbuild.ts +++ b/scripts/postbuild.ts @@ -1,6 +1,6 @@ /** * Postbuild script to process the lit-action.js file - * Purpose: Reads the compiled lit-action.js, escapes backticks, and wraps it in a module export + * Purpose: Reads the compiled lit-action.js, removes Ajv code, and converts it to a string export * Usage: Run after build process to prepare the lit-action code for distribution */ @@ -8,9 +8,12 @@ import fs from "fs"; console.log("- postbuilding..."); const actionCode = fs.readFileSync("./dist/lit-action.js", "utf-8"); -// Escape both backticks and template literal expressions -const escapedActionCode = actionCode - .replace(/`/g, "\\`") - .replace(/\${/g, "\\${"); -const code = `export const litActionCodeString = \`${escapedActionCode}\`;`; -fs.writeFileSync("./dist/lit-action.js", code); + +// Create a JavaScript string literal with the code properly escaped +// using JSON.stringify to handle all escaping correctly +const codeAsString = JSON.stringify(actionCode); + +// Create a regular string concatenation without template literals +const outputCode = "export const litActionCodeString = " + codeAsString + ";"; + +fs.writeFileSync("./dist/lit-action.js", outputCode); \ No newline at end of file