@lit-protocol/vincent-scaffold-sdk
Version:
Shared build configuration and utilities for Vincent abilities and policies
387 lines (318 loc) • 11.1 kB
Markdown
# 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.