diff --git a/packages/lit-agent-cli/src/commands/pkp/add-tool.ts b/packages/lit-agent-cli/src/commands/pkp/add-tool.ts index e54ce701..d7a3ead0 100644 --- a/packages/lit-agent-cli/src/commands/pkp/add-tool.ts +++ b/packages/lit-agent-cli/src/commands/pkp/add-tool.ts @@ -19,7 +19,18 @@ async function promptForPolicy( // Handle each property in sequence for (const [key, prop] of Object.entries(schema.properties)) { - if (prop.type === "array") { + if (prop.type === "boolean") { + // Special handling for boolean values + const { value } = await inquirer.prompt<{ value: boolean }>([ + { + type: "confirm", + name: "value", + message: prop.description, + default: prop.default, + }, + ]); + answers[key] = value; + } else if (prop.type === "array") { console.log(`\n${prop.description}`); if (prop.example) { console.log("Examples:", JSON.stringify(prop.example, null, 2)); diff --git a/packages/lit-agent-cli/src/core/agent/index.ts b/packages/lit-agent-cli/src/core/agent/index.ts index 249c6ddb..e7f76558 100644 --- a/packages/lit-agent-cli/src/core/agent/index.ts +++ b/packages/lit-agent-cli/src/core/agent/index.ts @@ -106,8 +106,7 @@ export async function processAgentRequest( rpcUrl: chainToSubmitTxnOnRpcUrl, chainId: parseInt(chainToSubmitTxnOnChainId), }, - publicKey: config.pkp!.publicKey!, - pkpEthAddress: config.pkp!.ethAddress!, + pkp: config.pkp, params: { ...analysis, user: ethersSigner.address, diff --git a/packages/lit-agent-cli/src/utils/tools.ts b/packages/lit-agent-cli/src/utils/tools.ts index 65562322..2d10f503 100644 --- a/packages/lit-agent-cli/src/utils/tools.ts +++ b/packages/lit-agent-cli/src/utils/tools.ts @@ -7,6 +7,15 @@ import { decodeSwapPolicy, } from "lit-agent-tool-uniswap"; +import { + signerMetadata, + signerLitActionDescription, + SignerPolicy, + signerPolicySchema, + encodeSignerPolicy, + decodeSignerPolicy, +} from "lit-agent-tool-signer"; + export interface LitAgentTool { name: string; description: string; @@ -28,5 +37,14 @@ export const getAvailableTools = (): LitAgentTool[] => { encodePolicyFn: encodeSwapPolicy, decodePolicyFn: decodeSwapPolicy, }, + { + name: "Signer", + description: signerLitActionDescription, + ipfsId: signerMetadata.signerLitAction.IpfsHash, + package: "lit-agent-tool-signer", + policySchema: signerPolicySchema, + encodePolicyFn: encodeSignerPolicy, + decodePolicyFn: decodeSignerPolicy, + }, ]; }; diff --git a/packages/lit-agent-tool-signer/.env.example b/packages/lit-agent-tool-signer/.env.example new file mode 100644 index 00000000..f2b66dad --- /dev/null +++ b/packages/lit-agent-tool-signer/.env.example @@ -0,0 +1 @@ +PINATA_JWT= \ No newline at end of file diff --git a/packages/lit-agent-tool-signer/.gitignore b/packages/lit-agent-tool-signer/.gitignore new file mode 100644 index 00000000..05a9d0cf --- /dev/null +++ b/packages/lit-agent-tool-signer/.gitignore @@ -0,0 +1 @@ +!dist/ \ No newline at end of file diff --git a/packages/lit-agent-tool-signer/README.md b/packages/lit-agent-tool-signer/README.md new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/lit-agent-tool-signer/README.md @@ -0,0 +1 @@ + diff --git a/packages/lit-agent-tool-signer/package.json b/packages/lit-agent-tool-signer/package.json new file mode 100644 index 00000000..017acaad --- /dev/null +++ b/packages/lit-agent-tool-signer/package.json @@ -0,0 +1,29 @@ +{ + "name": "lit-agent-tool-signer", + "version": "0.1.0", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "scripts": { + "build": "tsc && node scripts/build.js", + "deploy": "yarn build && node scripts/deploy.js" + }, + "dependencies": { + "@dotenvx/dotenvx": "^1.31.0", + "ethers": "v5", + "node-fetch": "^3.3.2" + }, + "devDependencies": { + "esbuild": "^0.20.1", + "typescript": "^5.0.0" + } +} diff --git a/packages/lit-agent-tool-signer/scripts/build.js b/packages/lit-agent-tool-signer/scripts/build.js new file mode 100644 index 00000000..fd0ecb1f --- /dev/null +++ b/packages/lit-agent-tool-signer/scripts/build.js @@ -0,0 +1,168 @@ +import * as esbuild from "esbuild"; +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import fs from "fs/promises"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = join(__dirname, ".."); + +// Function to get the description from index.ts +async function getDescription() { + const result = await esbuild.build({ + entryPoints: [join(rootDir, "src", "index.ts")], + bundle: false, + write: false, + format: "esm", + target: "esnext", + platform: "neutral", + }); + + const code = result.outputFiles[0].text; + const match = code.match( + /const signerLitActionDescription\s*=\s*["']([^"']+)["']/ + ); + + if (!match) { + console.error("Code content:", code); + throw new Error("Could not find description in index.ts"); + } + + return match[1]; +} + +// Function to generate the action string +async function generateActionString() { + // Build the action file to get its contents + const result = await esbuild.build({ + entryPoints: [join(rootDir, "src", "litAction.ts")], + bundle: true, // Enable bundling to resolve imports + write: false, + format: "esm", + target: "esnext", + platform: "neutral", + minify: false, + define: { + "process.env.NODE_ENV": '"production"', + }, + }); + + const actionCode = result.outputFiles[0].text; + + // Extract everything between the var assignment and the export statement + const startMatch = actionCode.indexOf("var litAction_default = "); + const endMatch = actionCode.indexOf("export {"); + + if (startMatch === -1 || endMatch === -1) { + console.error("Compiled code:", actionCode); + throw new Error("Could not find function boundaries in compiled code"); + } + + // Extract the function definition (excluding the variable assignment) + const functionBody = actionCode + .slice(startMatch + "var litAction_default = ".length, endMatch) + .trim() + .replace(/;$/, ""); // Remove trailing semicolon if present + + // Create self-executing function + return `(${functionBody})();`; +} + +// Function to get existing metadata +async function getExistingMetadata() { + try { + const content = await fs.readFile( + join(rootDir, "dist", "ipfs.json"), + "utf-8" + ); + const metadata = JSON.parse(content); + return metadata.signerLitAction || {}; + } catch (error) { + return {}; + } +} + +// Function to generate the index files +async function generateIndexFiles(ipfsMetadata = {}) { + const [actionString, description, existingMetadata] = await Promise.all([ + generateActionString(), + getDescription(), + getExistingMetadata(), + ]); + + // Use existing metadata if no new metadata is provided + const metadata = + Object.keys(ipfsMetadata).length > 0 + ? ipfsMetadata.signerLitAction + : existingMetadata; + + // Create the JavaScript content + const jsContent = ` +export const signerLitActionDescription = ${JSON.stringify(description)}; + +export const signerLitAction = ${JSON.stringify(actionString)}; + +export const signerMetadata = { + signerLitAction: { + IpfsHash: ${JSON.stringify(metadata.IpfsHash || "")}, + PinSize: ${metadata.PinSize || 0}, + Timestamp: ${JSON.stringify(metadata.Timestamp || "")}, + isDuplicate: ${metadata.isDuplicate || false}, + Duration: ${metadata.Duration || 0} + } +}; + +export * from "./policy"; +`; + + // Create the TypeScript declaration content + const dtsContent = ` +export type SignerMetadata = { + IpfsHash: string; + PinSize: number; + Timestamp: string; + isDuplicate: boolean; + Duration: number; +}; + +export type SignerLitActionString = string; + +export declare const signerLitActionDescription: string; +export declare const signerLitAction: SignerLitActionString; +export declare const signerMetadata: { + signerLitAction: SignerMetadata; +}; + +export * from "./policy"; +`; + + // Write the files + await Promise.all([ + fs.writeFile(join(rootDir, "dist", "index.js"), jsContent), + fs.writeFile(join(rootDir, "dist", "index.d.ts"), dtsContent), + fs.writeFile(join(rootDir, "dist", "litAction.js"), actionString), + ]); +} + +// Main build function +async function build() { + try { + // Ensure dist directory exists + await fs.mkdir(join(rootDir, "dist"), { recursive: true }); + + // Generate index files and write action string + await generateIndexFiles(); + + console.log("Build completed successfully"); + } catch (error) { + console.error("Build failed:", error); + process.exit(1); + } +} + +export { generateIndexFiles }; + +// Only run build if this is the main module +if (import.meta.url === `file://${process.argv[1]}`) { + build(); +} \ No newline at end of file diff --git a/packages/lit-agent-tool-signer/scripts/deploy.js b/packages/lit-agent-tool-signer/scripts/deploy.js new file mode 100644 index 00000000..bac2bac0 --- /dev/null +++ b/packages/lit-agent-tool-signer/scripts/deploy.js @@ -0,0 +1,137 @@ +import { fileURLToPath } from "url"; +import { dirname, join } from "path"; +import fs from "fs/promises"; +import fetch from "node-fetch"; +import dotenvx from "@dotenvx/dotenvx"; +import { generateIndexFiles } from "./build.js"; + +// Load environment variables +dotenvx.config(); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); +const rootDir = join(__dirname, ".."); + +async function uploadToPinata(pinataJwt, data) { + // Create boundary for multipart form data + const boundary = + "----WebKitFormBoundary" + Math.random().toString(36).substring(2); + + // Create form data manually + const formData = [ + `--${boundary}`, + 'Content-Disposition: form-data; name="file"; filename="litAction.js"', + "Content-Type: text/plain", + "", + data, + `--${boundary}`, + 'Content-Disposition: form-data; name="pinataMetadata"', + "", + JSON.stringify({ name: "Signer Lit Action" }), + `--${boundary}`, + 'Content-Disposition: form-data; name="pinataOptions"', + "", + JSON.stringify({ cidVersion: 0 }), + `--${boundary}--`, + ].join("\r\n"); + + const response = await fetch( + "https://api.pinata.cloud/pinning/pinFileToIPFS", + { + method: "POST", + headers: { + "Content-Type": `multipart/form-data; boundary=${boundary}`, + Authorization: `Bearer ${pinataJwt}`, + }, + body: formData, + } + ); + + if (!response.ok) { + const text = await response.text(); + throw new Error(`Pinata upload failed: ${response.status} - ${text}`); + } + + return response.json(); +} + +async function updateIpfsMetadata(newMetadata) { + try { + // Ensure dist directory exists + await fs.mkdir(join(rootDir, "dist"), { recursive: true }); + + let metadata = {}; + const ipfsPath = join(rootDir, "dist", "ipfs.json"); + + try { + const content = await fs.readFile(ipfsPath, "utf-8"); + metadata = JSON.parse(content); + } catch (error) { + // File doesn't exist or is invalid, start with empty object + } + + metadata["signerLitAction"] = newMetadata; + await fs.writeFile(ipfsPath, JSON.stringify(metadata, null, 2)); + + // Update index files with new metadata + await generateIndexFiles(metadata); + } catch (error) { + console.error("Failed to update ipfs.json:", error); + throw error; + } +} + +async function main() { + const PINATA_JWT = process.env.PINATA_JWT; + + if (!PINATA_JWT) { + console.error("Missing PINATA_JWT environment variable"); + process.exit(1); + } + + try { + // Ensure dist directory exists + await fs.mkdir(join(rootDir, "dist"), { recursive: true }); + + // First generate the files + console.log("Generating files..."); + await generateIndexFiles(); + + // Give the filesystem a moment to sync + console.log("Waiting for filesystem to sync..."); + await new Promise((resolve) => setTimeout(resolve, 2000)); + + // Read the action string + const litActionPath = join(rootDir, "dist", "litAction.js"); + console.log('Reading from:', litActionPath); + const actionString = await fs.readFile(litActionPath, "utf-8"); + + // Verify the content looks correct + if (!actionString.startsWith("(async () =>")) { + console.error("Generated code appears malformed:", actionString.substring(0, 100)); + throw new Error("Generated code is not in the expected format"); + } + + const startTime = Date.now(); + const pinataResponse = await uploadToPinata(PINATA_JWT, actionString); + const duration = (Date.now() - startTime) / 1000; + + // Create metadata + const metadata = { + IpfsHash: pinataResponse.IpfsHash, + PinSize: pinataResponse.PinSize, + Timestamp: new Date().toISOString(), + isDuplicate: pinataResponse.isDuplicate || false, + Duration: duration, + }; + + await updateIpfsMetadata(metadata); + console.log("Deployment successful:", metadata); + process.exit(0); + } catch (error) { + console.error("Deployment failed:", error); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/packages/lit-agent-tool-signer/src/index.ts b/packages/lit-agent-tool-signer/src/index.ts new file mode 100644 index 00000000..bdc44d77 --- /dev/null +++ b/packages/lit-agent-tool-signer/src/index.ts @@ -0,0 +1,27 @@ +export type SignerMetadata = { + IpfsHash: string; + PinSize: number; + Timestamp: string; + isDuplicate: boolean; + Duration: number; +}; + +// This represents the actual Lit Action code as a string +export type SignerLitActionString = string; + +export const signerLitActionDescription = "Lit Action for signing arbitrary messages"; + +// These will be populated by the build process +export const signerLitAction: SignerLitActionString = ""; +export const signerMetadata: { signerLitAction: SignerMetadata } = { + signerLitAction: { + IpfsHash: "", + PinSize: 0, + Timestamp: "", + isDuplicate: false, + Duration: 0, + }, +}; + +// Export policy types and functions +export * from "./policy"; diff --git a/packages/lit-agent-tool-signer/src/litAction.ts b/packages/lit-agent-tool-signer/src/litAction.ts new file mode 100644 index 00000000..e9c1791a --- /dev/null +++ b/packages/lit-agent-tool-signer/src/litAction.ts @@ -0,0 +1,81 @@ +//@ts-nocheck +export default async () => { + try { + // Check if we have the required parameters + if (!params.inputString) { + throw new Error("Missing required parameter: inputString"); + } + + // Policy Checks + const LIT_AGENT_REGISTRY_ABI = [ + "function getActionPolicy(address user, address pkp, string calldata ipfsCid) external view returns (bool isPermitted, bytes memory description, bytes memory policy)", + ]; + const LIT_AGENT_REGISTRY_ADDRESS = "0x728e8162603F35446D09961c4A285e2643f4FB91"; + + // Validate auth parameters + if (!params.user) { + throw new Error("Missing required parameter: user"); + } + if (!params.ipfsCid) { + throw new Error("Missing required parameter: ipfsCid"); + } + if (!pkp.ethAddress) { + throw new Error("Missing required parameter: pkp.ethAddress"); + } + + // Create contract instance + const registryContract = new ethers.Contract( + LIT_AGENT_REGISTRY_ADDRESS, + LIT_AGENT_REGISTRY_ABI, + new ethers.providers.JsonRpcProvider(chainInfo.rpcUrl) + ); + + const [isPermitted, , policy] = await registryContract.getActionPolicy( + params.user, + pkp.ethAddress, + params.ipfsCid + ); + + if (!isPermitted) { + throw new Error("Action not permitted for this PKP"); + } + + // Decode the policy + const policyStruct = ["tuple(bool allowAll)"]; + let decodedPolicy; + try { + decodedPolicy = ethers.utils.defaultAbiCoder.decode(policyStruct, policy)[0]; + + if (!decodedPolicy.allowAll) { + throw new Error("Signing is not allowed by policy"); + } + } catch (error) { + throw new Error( + `Failed to decode policy: ${error instanceof Error ? error.message : String(error)}` + ); + } + + // Sign the message + const signature = await Lit.Actions.signEcdsa({ + toSign: ethers.utils.arrayify( + ethers.utils.keccak256(ethers.utils.toUtf8Bytes(params.inputString)) + ), + publicKey: pkp.publicKey, + sigName: "sig", + }); + + Lit.Actions.setResponse({ + response: JSON.stringify({ + status: "success", + }), + }); + } catch (error) { + console.error("Error:", error); + Lit.Actions.setResponse({ + response: JSON.stringify({ + status: "error", + error: error.message, + }), + }); + } +}; \ No newline at end of file diff --git a/packages/lit-agent-tool-signer/src/policy.ts b/packages/lit-agent-tool-signer/src/policy.ts new file mode 100644 index 00000000..f2db75c1 --- /dev/null +++ b/packages/lit-agent-tool-signer/src/policy.ts @@ -0,0 +1,56 @@ +import { ethers } from "ethers"; + +/** + * Type definition for the Signer Policy + * This matches the Solidity struct: + * struct SignerPolicy { + * bool allowAll; + * } + */ +export interface SignerPolicy { + allowAll: boolean; +} + +/** + * Schema for the Signer Policy, used for CLI prompts + */ +export const signerPolicySchema = { + type: "object", + properties: { + allowAll: { + type: "boolean", + description: "WARNING: This will allow the PKP to sign ANY message. Are you sure you want to enable unrestricted signing?", + default: false, + }, + }, + required: ["allowAll"], +}; + +/** + * Validates and encodes a SignerPolicy into the format expected by the Lit Action + * @param policy The policy to encode + * @returns The ABI encoded policy bytes + */ +export function encodeSignerPolicy(policy: SignerPolicy): string { + // Encode the policy using a simple boolean + return ethers.utils.defaultAbiCoder.encode( + ["tuple(bool allowAll)"], + [{ allowAll: policy.allowAll }] + ); +} + +/** + * Decodes an ABI encoded signer policy + * @param encodedPolicy The ABI encoded policy bytes + * @returns The decoded SignerPolicy object + */ +export function decodeSignerPolicy(encodedPolicy: string): SignerPolicy { + const decoded = ethers.utils.defaultAbiCoder.decode( + ["tuple(bool allowAll)"], + encodedPolicy + )[0]; + + return { + allowAll: decoded.allowAll, + }; +} diff --git a/packages/lit-agent-tool-signer/tsconfig.json b/packages/lit-agent-tool-signer/tsconfig.json new file mode 100644 index 00000000..eebc1020 --- /dev/null +++ b/packages/lit-agent-tool-signer/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "esModuleInterop": true, + "strict": true, + "skipLibCheck": true, + "outDir": "./dist", + "rootDir": "./src", + "declaration": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/litAction.ts"] +} diff --git a/packages/lit-agent-toolkit/src/agent.ts b/packages/lit-agent-toolkit/src/agent.ts index 62b02425..87e85ba0 100644 --- a/packages/lit-agent-toolkit/src/agent.ts +++ b/packages/lit-agent-toolkit/src/agent.ts @@ -41,8 +41,9 @@ export async function analyzeUserIntentAndMatchAction( - recommendedCID: the exact ipfsCid if there's a match, or "" if no clear match - tokenIn: (for swaps) the input token address as a string - tokenOut: (for swaps) the output token address as a string - - amountIn: the input amount as a string + - amountIn: (for swaps)the input amount as a string - recipientAddress: (for sends) the recipient address as a string + - inputString: (for signing) the input string as a string Do not nest parameters in a 'parameters' object.`, },