UNPKG

@lit-protocol/vincent-scaffold-sdk

Version:

Shared build configuration and utilities for Vincent abilities and policies

562 lines (461 loc) 23.8 kB
# Vincent Framework: ERC-20 Transfer Ability Implementation ## Context & Existing Architecture This Vincent Starter Kit contains a sophisticated ability and policy system with the following components: ### Current Implementation - **`native-send` ability** (`./vincent-packages/abilities/native-send/`) - Handles native ETH transfers - **`send-counter-limit` policy** (`./vincent-packages/policies/send-counter-limit/`) - Enforces transfer limits using smart contract state management - **E2E test suite** (`./vincent-e2e/src/e2e.ts`) - Demonstrates full integration testing between abilities and policies ### Architecture Pattern - **Abilities** use Lit Actions with `@lit-protocol/vincent-ability-sdk` framework - **Ability hooks**: `precheck` (validation) and `execute` (transaction execution) - **Policy hooks**: `precheck`, `evaluate`, and `commit` for state management - **Type safety**: Zod schemas for strict input/output validation and type inference - **Ability schemas**: `./vincent-packages/abilities/native-send/src/lib/schemas.ts` - **Policy schemas**: `./vincent-packages/policies/send-counter-limit/src/lib/schemas.ts` - **Pattern**: Each component defines its own validation schemas for parameters and results - **Blockchain operations**: `laUtils` from `@lit-protocol/vincent-scaffold-sdk/la-utils` ### Schema Architecture The existing implementation uses comprehensive Zod schemas for type safety: **Ability Parameter Validation Pattern:** ```typescript // Example from native-send ability schemas 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"), }); ``` **Result Schemas Pattern:** ```typescript // Success/failure schemas for precheck and execute operations export const precheckSuccessSchema = z.object({...}); export const precheckFailSchema = z.object({...}); export const executeSuccessSchema = z.object({...}); export const executeFailSchema = z.object({...}); ``` ## Task: Create ERC-20 Transfer Ability ### Objective Create a new ability called `erc20-transfer` by copying and adapting the existing `native-send` ability structure to handle ERC-20 token transfers. ### Implementation Requirements #### 1. Directory Structure & File Creation Checklist **CRITICAL**: Create these exact files by copying from `native-send` and modifying: ``` vincent-packages/abilities/erc20-transfer/ ├── package.json # ✅ Copy and update package name ├── tsconfig.json # ✅ Copy as-is ├── README.md # ✅ Copy and update description ├── global.d.ts # ✅ Copy as-is ├── src/ │ ├── index.ts # ✅ Copy as-is (exports generated bundle) │ └── lib/ │ ├── schemas.ts # ✅ Copy and modify for ERC-20 parameters │ ├── vincent-ability.ts # ✅ Copy and modify for ERC-20 logic │ └── helpers/ │ ├── index.ts # ✅ CREATE NEW - ERC-20 specific helpers │ └── commit-allowed-policies.ts # ✅ Copy from existing ability ``` #### 1. Directory Structure - Create `./vincent-packages/abilities/erc20-transfer/` following the exact same structure as `native-send` - Copy all configuration files and update appropriately - Create `src/lib/helpers/index.ts` for any ERC-20 specific helper functions (following the pattern from policies) #### 2. Package Configuration - **Package name**: `@agentic-ai/vincent-ability-erc20-transfer` (exact format) - **Main files to update**: - `package.json`: Update `name`, `description`, keep all other dependencies identical - `tsconfig.json`: Copy exactly as-is from `native-send` - `README.md`: Update title and description for ERC-20 functionality - `global.d.ts`: Copy exactly as-is from `native-send` #### 3. Ability Parameters Schema (`src/lib/schemas.ts`) Extend the existing native-send schema pattern to include token contract address and network configuration. It shuold allows user to enter amount, tokenAddress, tokenDecimals, rpcUrl and chainId **Additional Required Schemas:** - Copy all result schemas from `native-send` (`precheckSuccessSchema`, `executeSuccessSchema`, etc.) - Update schema descriptions to reflect ERC-20 functionality - Maintain the same type export patterns #### 4. Ability Implementation (`src/lib/vincent-ability.ts`) **CRITICAL Import Patterns** (copy exactly): ```typescript import { createVincentAbility, createVincentPolicy, supportedPoliciesForAbility, } from "@lit-protocol/vincent-ability-sdk"; import { bundledVincentPolicy } from "../../../../policies/send-counter-limit/dist/index.js"; import { laUtils } from "@lit-protocol/vincent-scaffold-sdk"; ``` **Policy Integration Pattern** (copy exactly): ```typescript const SendLimitPolicy = createVincentPolicy({ abilityParamsSchema, bundledVincentPolicy, abilityParameterMappings: { to: "to", amount: "amount", }, }); ``` **Ability Creation Pattern** (copy and modify): ```typescript export const vincentAbility = createVincentAbility({ packageName: "@agentic-ai/vincent-ability-erc20-transfer" as const, abilityDescription: "ERC-20 transfer ability", abilityParamsSchema, supportedPolicies: supportedPoliciesForAbility([SendLimitPolicy]), precheckSuccessSchema, precheckFailSchema, executeSuccessSchema, executeFailSchema, precheck: async ({ abilityParams }, { succeed, fail }) => { /* ... */ }, execute: async ( { abilityParams }, { succeed, fail, delegation, policiesContext } ) => { /* ... */ }, }); ``` - **Precheck function**: Validate recipient address, amount, token contract address, and network parameters - **CRITICAL Balance Validations**: Both native and ERC-20 balance checks must be performed in precheck before attempting execution - **Native Balance Check**: Verify sender has sufficient native tokens for gas fees - Create ethers provider: `new ethers.providers.JsonRpcProvider(abilityParams.rpcEndpoint, abilityParams.chainId)` - Get sender balance: `await provider.getBalance(senderAddress)` - Estimate gas costs for ERC-20 transfer transaction - Validate: `nativeBalance >= estimatedGasCost` - Return descriptive error if insufficient native balance for gas - **ERC-20 Balance Check**: Verify sender has sufficient ERC-20 tokens to transfer - Create ERC-20 contract instance using token address and standard ERC-20 ABI - Call `balanceOf(senderAddress)` to get current token balance - Parse amount considering token decimals: `ethers.utils.parseUnits(amount, tokenDecimals)` - Validate: `tokenBalance >= requestedAmount` - Return descriptive error if insufficient ERC-20 token balance - **Gas Estimation**: Use contract.estimateGas.transfer() for accurate gas estimation - **Error Messages**: Provide clear, user-friendly error messages for each failure scenario - **CRITICAL**: These balance checks must be performed in precheck before attempting execution - **Execute function**: Use `laUtils.transaction.handler.contractCall()` to call ERC-20 contract's `transfer` function - **Provider Configuration**: **CRITICAL** - The provider MUST be configurable by the ability consumer (e2e test), NOT hardcoded in the ability - **FORBIDDEN**: Do NOT hardcode any RPC endpoints in the ability implementation - **REQUIRED**: Ability parameters MUST include `rpcUrl` and `chainId` so the e2e test can specify which chain to use - **Pattern**: `const provider = new ethers.providers.JsonRpcProvider(abilityParams.rpcUrl, abilityParams.chainId)` - **Execute function pattern**: ```typescript const { to, amount, tokenAddress, tokenDecimals, rpcUrl, chainId } = abilityParams; abilityParams; const provider = new ethers.providers.JsonRpcProvider(rpcUrl, chainId); ``` - **Helper functions**: Create ERC-20 specific helpers in `src/lib/helpers/index.ts` for: - **ERC-20 ABI definitions**: Standard ERC-20 ABI including `transfer`, `balanceOf`, `decimals` functions - **Balance check utilities**: - `checkNativeBalance(provider, address, estimatedGasCost)`: Validates sufficient native tokens for gas - `checkERC20Balance(provider, tokenAddress, ownerAddress, amount)`: Validates sufficient ERC-20 tokens - `getTokenDecimals(provider, tokenAddress)`: Retrieves token decimal places for proper amount parsing - **Amount parsing/validation utilities**: Handle token decimal conversion and validation - **Gas estimation helpers**: Calculate gas costs for ERC-20 transfers - **Contract interaction helpers**: Create contract instances and handle common ERC-20 operations - **Available laUtils APIs**: - `laUtils.transaction.handler.contractCall()` - Execute contract calls ```ts // interface export const contractCall = async ({ provider, pkpPublicKey, callerAddress, abi, contractAddress, functionName, args, overrides = {}, chainId, }: { provider: any; pkpPublicKey: string; callerAddress: string; abi: any[]; contractAddress: string; functionName: string; args: any[]; overrides?: { value?: string | number | bigint; gasLimit?: number; }; chainId?: number; }) => { ``` - `laUtils.helpers.toEthAddress()` - Address utilities - **Policy integration**: Maintain same pattern as `native-send` for `send-counter-limit` policy - **Policy commit pattern**: Use the helper function for cleaner code ```typescript const policyCommitResults = await commitAllowedPolicies( policiesContext, "[@agentic-ai/vincent-ability-erc20-transfer/execute]" ); ``` - **Error handling**: Follow existing logging and error patterns - **Console logging format**: Use emojis in console logs with emojis placed AFTER the `[ ]` bracket, not before - **Pattern**: `console.log("[@ability-name] ✅ Success message")` (correct) - **NOT**: `console.log("✅ [@ability-name] Success message")` (incorrect) - **Visual indicators**: Use ✅ for success, ❌ for errors, 🔍 for validation, 🚀 for execution, etc. - **Configurable parameters principle**: Any value that could vary between different use cases MUST be configurable via ability parameters - **Include in schema**: All configurable values must be defined in `abilityParamsSchema` and editable from tests - **No hardcoding**: Avoid hardcoding values that could reasonably be different for different tokens, networks, or use cases - **Examples of what should be configurable**: - Token decimals (varies by token: USDC=6, WETH=18, etc.) - Gas limits and pricing preferences - Slippage tolerance for DEX operations - Network-specific parameters (RPC endpoints, chain IDs) - Token-specific parameters (contract addresses, decimal places) - **Test flexibility**: E2E tests should be able to easily test different scenarios by changing parameter values - **Parameter mapping**: Ensure policy receives the same parameter structure (to, amount) for compatibility #### 5. Required Helper Functions (`src/lib/helpers/index.ts`) **CRITICAL**: These helper functions must be implemented to support the balance validation requirements: - ERC20_ABI - check native balance - check erc20 balance Add any necessary helper functions if required. #### 6. Technical Constraints - **CRITICAL**: `laUtils` can ONLY be used in Lit Action environment - Inside ability's `execute` hook - Inside policy's `evaluate` hook - NOT available in `precheck` hooks - **Precheck Implementation Pattern**: Since `laUtils` is not available in precheck, use direct ethers.js for balance validations - Create provider directly: `new ethers.providers.JsonRpcProvider(abilityParams.rpcEndpoint, abilityParams.chainId)` - Use standard ethers contracts for ERC-20 interactions - Perform all validation logic before the execute phase - **Example precheck structure**: ```typescript precheck: async ({ abilityParams, senderPkpEthAddress, success, fail }) => { try { const provider = new ethers.providers.JsonRpcProvider( abilityParams.rpcEndpoint, abilityParams.chainId ); // Check native balance for gas const nativeBalance = await provider.getBalance(senderPkpEthAddress); const gasEstimate = await estimateERC20TransferGas( provider, abilityParams ); if (nativeBalance.lt(gasEstimate)) { return fail({ message: "Insufficient native token balance for gas fees", }); } // Check ERC-20 token balance const tokenBalance = await getERC20Balance( provider, abilityParams.tokenAddress, senderPkpEthAddress ); const transferAmount = ethers.utils.parseUnits( abilityParams.amount, await getTokenDecimals(provider, abilityParams.tokenAddress) ); if (tokenBalance.lt(transferAmount)) { return fail({ message: "Insufficient ERC-20 token balance" }); } return success({ message: "Balance checks passed" }); } catch (error) { return fail({ message: `Precheck failed: ${error.message}` }); } }; ``` - **Network Configuration**: **CRITICAL** - Ability must be chain-agnostic and configurable by consumer - **FORBIDDEN**: Do NOT hardcode any RPC endpoints or chain IDs in the ability - **REQUIRED**: Ability parameters must include `rpcEndpoint` and `chainId` for full flexibility - **Support multiple chains**: Base, Ethereum, Polygon, etc. - determined by ability consumer - **E2E test responsibility**: The e2e test specifies which chain to use via ability parameters - **Example configurations**: - Base (recommended): `rpcEndpoint: "https://base.llamarpc.com"`, `chainId: 8453` - Ethereum: `rpcEndpoint: "https://eth.llamarpc.com"`, `chainId: 1` - Polygon: `rpcEndpoint: "https://polygon.llamarpc.com"`, `chainId: 137` - **Default E2E testing**: Use Base network with USDC token (`0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`) - **FORBIDDEN**: Do NOT use the following in abilities and policies: - `globalThis` - Not available in Lit Action environment - `process.env` - Environment variables not accessible in Lit Actions - **Mock data** - Absolutely no fake/mock data just to make things work - **CRITICAL**: **DEFINITELY SHOULD NOT** be mocking any data whatsoever. If anything is missing or unclear during implementation, ask for clarification and provide a proper solution rather than creating mock/fake data - **Error handling pattern**: In ability `execute()` and policy `evaluate()` hooks, **MUST NOT throw errors** - Use `return fail()` instead of throwing exceptions - Framework expects structured error responses via fail() callback - Throwing errors will break the execution flow - **Available laUtils APIs**: ```typescript export const laUtils = { transaction: { handler: { contractCall, // ← Use this for ERC-20 transfers nativeSend, }, primitive: { getNonce, sendTx, signTx, }, }, helpers: { toEthAddress, // ← Use this for PKP address conversion }, }; ``` - **CRITICAL**: Use `laUtils` import: `import { laUtils } from "@lit-protocol/vincent-scaffold-sdk";` (NOT `/la-utils`) - **Contract call pattern for ERC-20**: ```typescript const txHash = await laUtils.transaction.handler.contractCall({ provider, pkpPublicKey, callerAddress, contractAddress: tokenAddress, abi: ERC20_TRANSFER_ABI, functionName: "transfer", args: [to, tokenAmountInWei], chainId: finalChainId, }); ``` - Follow existing code conventions and patterns - Maintain exact same schema validation approach #### 7. Integration Requirements - **Policy compatibility**: Work with existing `send-counter-limit` policy without modifications - **E2E testing**: Update test suite to include ERC-20 transfer scenarios - **App configuration**: Ensure ability can be registered using same pattern as `native-send` - **Schema consistency**: Follow the established schema patterns from existing abilities **E2E Test Integration Pattern:** Create a new file `vincent-e2e/src/e2e-erc20.ts` (copy `e2e.ts` structure exactly), then update imports: ```typescript import { vincentPolicyMetadata as sendLimitPolicyMetadata } from "../../vincent-packages/policies/send-counter-limit/dist/index.js"; import { bundledVincentAbility as erc20TransferAbility } from "../../vincent-packages/abilities/erc20-transfer/dist/index.js"; ``` **E2E Test Ability Parameters Pattern**: ```typescript const TEST_ABILITY_PARAMS = { to: accounts.delegatee.ethersWallet.address, // Transfer to self for testing amount: "0.000001", tokenAddress: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // Base USDC Contract Address tokenDecimals: 6, rpcUrl: "https://base.llamarpc.com", chainId: 8453, }; ``` **E2E Test Client Pattern**: ```typescript const erc20TransferAbilityClient = getVincentAbilityClient({ bundledVincentAbility: erc20TransferAbility, ethersSigner: accounts.delegatee.ethersWallet, }); ``` **Key Points:** - Use relative paths to `dist/` folders (not published packages) - Named exports: `bundledVincentAbility` for abilities, `vincentPolicyMetadata` for policies - Development workflow: build → import → test (no publishing required) - **Create separate E2E file**: Create a new `e2e-erc20.ts` file in `vincent-e2e/src/` using the exact same structure as the existing `e2e.ts` - **Package.json update**: Add new test script to root `package.json` for running the ERC-20 specific abilities - **Test pattern**: E2E test must perform ERC-20 transfer to self (same sender and recipient address) - **Network specification**: E2E test must specify the target chain via `rpcEndpoint` and `chainId` parameters - **Default testing configuration**: Use Base network with USDC token - **Example test configuration**: ```typescript const TEST_RPC_ENDPOINT = "https://base.llamarpc.com"; // Base network const TEST_CHAIN_ID = 8453; // Base mainnet const TEST_TOKEN_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"; // USDC on Base // E2E test calls include network config await erc20TransferAbilityClient.execute( { to: TEST_RECIPIENT, amount: TEST_AMOUNT, tokenAddress: TEST_TOKEN_ADDRESS, rpcEndpoint: TEST_RPC_ENDPOINT, chainId: TEST_CHAIN_ID, }, { delegatorPkpEthAddress: agentWalletPkp.ethAddress } ); ``` - **Preserve existing tests**: Keep the original `e2e.ts` file intact for native send functionality #### 8. Build Script Configuration **CRITICAL**: After creating the new ability, you MUST update the root `package.json` build script to include the new ability: **Current build script:** ```json "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'" ``` **Updated build script (must include erc20-transfer):** ```json "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'" ``` **Also add ERC-20 E2E test script:** ```json "vincent:e2e:erc20": "node vincent-e2e/src/e2e-erc20.ts" ``` **Why this is critical:** - The build script ensures all abilities and policies are properly compiled - Without updating this script, the new `erc20-transfer` ability won't be built automatically - This affects deployment and E2E testing functionality - The script builds dependencies in the correct order #### 9. Verification Testing **CRITICAL**: After completing the implementation and updating the build script, you MUST verify everything works correctly: **Step 1: Build Verification** ```bash npm run vincent:build ``` **What this does:** - Compiles all policies and abilities (including the new `erc20-transfer` ability) - Generates the necessary Lit Action files - Ensures TypeScript compilation succeeds **Expected outcome:** - Build completes successfully without errors - Generated files appear in `vincent-packages/abilities/erc20-transfer/src/generated/` - No TypeScript compilation errors **Step 2: E2E Test Verification** ```bash npm run vincent:e2e ``` **What this does:** - Runs the complete end-to-end test suite - Tests ability registration and policy integration - Validates that abilities work with the existing `send-counter-limit` policy - Confirms blockchain transactions execute successfully **Expected outcome:** - All existing tests pass (native-send functionality remains intact) - New ERC-20 transfer functionality integrates seamlessly - Policy limits are enforced correctly for both abilities **CRITICAL for E2E Success:** - Tests must execute the **`.execute()` function**, not just `.precheck()` - `.precheck()` only validates parameters - it's not sufficient for E2E verification - `.execute()` performs actual blockchain transactions and confirms end-to-end functionality - E2E tests are only considered successful when actual ERC-20 transfers complete on-chain **Verification Checklist:** - [ ] `npm run vincent:build` completes without errors - [ ] Generated files exist in `erc20-transfer/src/generated/` - [ ] `npm run vincent:e2e` passes all tests - [ ] Both native and ERC-20 transfers work with send limits - [ ] No regression in existing functionality **If tests fail:** 1. Check that the build script was updated correctly in `package.json` 2. Verify all files were copied and modified properly 3. Ensure the ability parameters match the expected schema 4. Confirm policy integration follows the same pattern as `native-send` **State Reset (Only if needed):** If you need to reset policy state data (e.g., send counters), you can run: ```bash npm run vincent:reset ``` This clears all policy state and should only be used when you specifically want to reset counters or when policy parameter values have been changed. ### Success Criteria - [ ] Ability builds successfully with `npm run vincent:build` - [ ] **Root package.json build script updated to include erc20-transfer** - [ ] **Verification tests pass: `npm run vincent:build` and `npm run vincent:e2e`** - [ ] **Provider is fully configurable by ability consumer** - NO hardcoded RPC endpoints in ability - [ ] **E2E test specifies target chain** via `rpcEndpoint` and `chainId` parameters - [ ] Integrates seamlessly with existing `send-counter-limit` policy - [ ] E2E tests pass for both native and ERC-20 transfers - [ ] Follows all existing code patterns and conventions - [ ] Proper error handling and detailed logging - [ ] Uses existing `laUtils` APIs with custom helpers for ERC-20 transfers - [ ] Maintains consistent schema validation patterns - [ ] **No forbidden patterns**: No `globalThis`, `process.env`, hardcoded RPC endpoints, or mock data in abilities/policies ### Reference Implementation Use the existing `native-send` ability as the primary template, especially: - **Schema structure**: `./vincent-packages/abilities/native-send/src/lib/schemas.ts` - **Ability implementation**: `./vincent-packages/abilities/native-send/src/lib/vincent-ability.ts` - **Policy integration patterns**: How `native-send` maps parameters to policies - **Transaction execution**: Using `laUtils` for blockchain operations - **Error handling and logging approaches**: Consistent messaging patterns