UNPKG

@lit-protocol/vincent-scaffold-sdk

Version:

Shared build configuration and utilities for Vincent abilities and policies

387 lines (318 loc) 11.1 kB
# Vincent Framework: Google Gemini Development Guide ## Quick Start for Gemini This Vincent Scaffold SDK creates blockchain abilities and policies that execute on Lit Actions. As Gemini, focus on TypeScript precision, schema validation, and blockchain integration patterns. ## 🚨 Critical Execution Environment Vincent abilities/policies run in **Lit Actions** - a restricted blockchain environment: ```typescript // ❌ These will FAIL in Lit Actions: process.env.API_KEY // No environment variables globalThis.fetch // Limited global objects localStorage.setItem() // No persistent storage require('fs') // No Node.js modules // ✅ Use these patterns instead: import { laUtils } from "@lit-protocol/vincent-scaffold-sdk/la-utils"; const provider = new ethers.providers.JsonRpcProvider( "https://yellowstone-rpc.litprotocol.com/" ); ``` ## 🏗️ Development Workflow ### 1. Schema-First Development Always start with Zod schemas for type safety: ```typescript import { z } from "zod"; // Define input validation export const abilityParamsSchema = z.object({ to: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid Ethereum address"), amount: z.string() .regex(/^\d*\.?\d+$/, "Invalid amount format") .refine((val) => parseFloat(val) > 0, "Amount must be greater than 0"), tokenAddress: z.string().regex(/^0x[a-fA-F0-9]{40}$/, "Invalid token contract") }); // Generate types export type AbilityParams = z.infer<typeof abilityParamsSchema>; // Result schemas for each operation export const executeSuccessSchema = z.object({ txHash: z.string(), timestamp: z.number(), gasUsed: z.number().optional() }); ``` ### 2. Ability Implementation Pattern ```typescript import { createVincentAbility } from "@lit-protocol/vincent-ability-sdk"; import { laUtils } from "@lit-protocol/vincent-scaffold-sdk/la-utils"; export const vincentAbility = createVincentAbility({ packageName: "{{packageName}}" as const, abilityDescription: "{{abilityDescription}}", abilityParamsSchema, // Phase 1: Input validation (NO blockchain access) precheck: async ({ abilityParams }, { succeed, fail }) => { // Pure validation logic if (!isValidEthereumAddress(abilityParams.to)) { return fail({ error: "Invalid recipient address" }); } if (parseFloat(abilityParams.amount) <= 0) { return fail({ error: "Amount must be positive" }); } return succeed({ validated: true, estimatedGas: 21000 }); }, // Phase 2: Blockchain execution (laUtils available) execute: async ({ abilityParams }, { succeed, fail, delegation }) => { try { // Get blockchain provider const provider = new ethers.providers.JsonRpcProvider( "https://yellowstone-rpc.litprotocol.com/" ); // Execute transaction using laUtils const txHash = await laUtils.transaction.handler.contractCall({ provider, pkpPublicKey: delegation.delegatorPkpInfo.publicKey, callerAddress: delegation.delegatorPkpInfo.ethAddress, abi: ERC20_ABI, contractAddress: abilityParams.tokenAddress, functionName: "transfer", args: [abilityParams.to, parseEther(abilityParams.amount)] }); return succeed({ txHash, to: abilityParams.to, amount: abilityParams.amount, timestamp: Date.now() }); } catch (error) { return fail({ error: error instanceof Error ? error.message : "Transaction failed" }); } } }); ``` ### 3. Policy Implementation Pattern ```typescript export const vincentPolicy = createVincentPolicy({ packageName: "{{packageName}}" as const, userParamsSchema, // Phase 1: Early validation precheck: async ({ abilityParams, userParams }, { allow, deny }) => { const { maxTransfers, timeWindowHours } = userParams; // Check current usage from external storage const currentCount = await getCurrentTransferCount( delegation.delegatorPkpInfo.ethAddress, timeWindowHours ); if (currentCount >= maxTransfers) { return deny({ reason: `Transfer limit exceeded: ${currentCount}/${maxTransfers}`, resetTime: getResetTimestamp(timeWindowHours) }); } return allow({ currentCount, remainingTransfers: maxTransfers - currentCount }); }, // Phase 2: Runtime evaluation (in Lit Actions) evaluate: async ({ abilityParams, userParams }, { allow, deny }) => { const result = await Lit.Actions.runOnce( { waitForResponse: true, name: "evaluateTransfer" }, async () => { // Blockchain-based validation return await validateTransferLimits( delegation.delegatorPkpInfo.ethAddress, userParams.maxTransfers, userParams.timeWindowHours ); } ); return result.allowed ? allow(result) : deny(result); }, // Phase 3: State recording commit: async ({ currentCount }, { allow, delegation }) => { try { // Record transaction to blockchain state const provider = new ethers.providers.JsonRpcProvider( "https://yellowstone-rpc.litprotocol.com/" ); await laUtils.transaction.handler.contractCall({ provider, pkpPublicKey: delegation.delegatorPkpInfo.publicKey, // Update policy state contract }); return allow({ recorded: true, newCount: currentCount + 1 }); } catch (error) { // Still allow if state recording fails return allow({ recorded: false, newCount: currentCount + 1 }); } } }); ``` ## 🛠️ Common Development Tasks ### Creating New Components ```bash # Create new ability npx @lit-protocol/vincent-scaffold-sdk add ability erc20-transfer # Create new policy npx @lit-protocol/vincent-scaffold-sdk add policy transfer-limits # Build all components npm run vincent:build # Run tests npm run vincent:e2e ``` ### Build Script Updates When adding new abilities/policies, update `package.json`: ```json { "scripts": { "vincent:build": "dotenv -e .env -- sh -c 'cd vincent-packages/policies/send-counter-limit && npm install && npm run build && cd ../../abilities/native-send && npm install && npm run build && cd ../erc20-transfer && npm install && npm run build'" } } ``` ### E2E Testing Pattern ```typescript // Import built components import { bundledVincentAbility as erc20Ability } from "../../vincent-packages/abilities/erc20-transfer/dist/index.js"; import { vincentPolicyMetadata as limitPolicy } from "../../vincent-packages/policies/transfer-limits/dist/index.js"; // Configure for testing const abilityConfig = createVincentAbilityConfig({ ability: erc20Ability, userParams: { to: "0x742d35Cc6635C0532925a3b8D400631707BFFfcc", amount: "0.001", tokenAddress: "0x1234567890123456789012345678901234567890" } }); // Execute with policy enforcement const result = await chainClient.executeAbilities({ abilities: [abilityConfig], policies: [limitPolicy] }); ``` ## 🎯 TypeScript Best Practices ### Strict Type Safety ```typescript // Use precise types interface TransferResult { readonly txHash: string; readonly gasUsed: bigint; readonly blockNumber: number; } // Leverage template literal types type EventName = `Transfer_${string}`; // Use branded types for addresses type EthereumAddress = string & { readonly __brand: unique symbol }; ``` ### Error Handling ```typescript // Structured error types interface AbilityError { readonly code: 'VALIDATION_ERROR' | 'EXECUTION_ERROR' | 'NETWORK_ERROR'; readonly message: string; readonly details?: Record<string, unknown>; } // Error result pattern const handleAbilityExecution = async (): Promise< { success: true; result: TransferResult } | { success: false; error: AbilityError } > => { try { const result = await executeTransfer(); return { success: true, result }; } catch (error) { return { success: false, error: { code: 'EXECUTION_ERROR', message: error instanceof Error ? error.message : 'Unknown error', details: { originalError: error } } }; } }; ``` ## 🔧 Available APIs ### laUtils Reference ```typescript // Chain utilities (Lit Actions only) const provider = new ethers.providers.JsonRpcProvider( "https://yellowstone-rpc.litprotocol.com/" ); const config = laUtils.chain.yellowstoneConfig; // Transaction handlers (Lit Actions only) const txHash = await laUtils.transaction.handler.contractCall({ provider, pkpPublicKey: "0x...", callerAddress: "0x...", abi: contractABI, contractAddress: "0x...", functionName: "transfer", args: [recipient, amount], overrides: { gasLimit: 100000 } }); // Native ETH transfers const nativeTxHash = await laUtils.transaction.handler.nativeSend({ provider, pkpPublicKey: "0x...", amount: "0.001", to: "0x..." }); // Helper utilities const ethAddress = laUtils.helpers.toEthAddress(publicKey); ``` ## ⚠️ Common Pitfalls to Avoid 1. **Using laUtils in precheck hooks** ```typescript // ❌ WRONG - laUtils not available in precheck precheck: async ({ abilityParams }, { succeed, fail }) => { const provider = new ethers.providers.JsonRpcProvider( "https://yellowstone-rpc.litprotocol.com/" ); // FAILS } // ✅ CORRECT - validation only precheck: async ({ abilityParams }, { succeed, fail }) => { if (!isValidAddress(abilityParams.to)) return fail({ error: "Invalid address" }); } ``` 2. **Forgetting build script updates** ```typescript // After creating new ability, MUST update package.json build script // Otherwise new ability won't be compiled ``` 3. **Mock data usage** ```typescript // ❌ NEVER use mock data const mockResult = { txHash: "0xfake..." }; // ✅ Ask for clarification if data unclear // Request proper test data or configuration ``` 4. **Incorrect import paths** ```typescript // ❌ WRONG - won't work in E2E tests import { myAbility } from "@company/vincent-ability-name"; // ✅ CORRECT - relative path to built output import { bundledVincentAbility } from "../../vincent-packages/abilities/name/dist/index.js"; ``` ## 🚀 Quick Reference Commands ```bash # Initialize project npx @lit-protocol/vincent-scaffold-sdk init # Add components npx @lit-protocol/vincent-scaffold-sdk add ability my-ability npx @lit-protocol/vincent-scaffold-sdk add policy my-policy # Development cycle npm run vincent:build # Compile all components npm run vincent:e2e # Run integration tests npm run vincent:reset # Reset test state # From ability/policy directory npx @lit-protocol/vincent-scaffold-sdk pkg build # Build current package npx @lit-protocol/vincent-scaffold-sdk pkg clean # Clean dist files ``` Remember: Vincent development requires precision due to blockchain execution constraints. Focus on type safety, proper error handling, and following established patterns.