Plugin Integration Guide
This guide walks you through the process of creating a new plugin for the edwin SDK. edwin is a TypeScript library that serves as a bridge between AI agents and DeFi protocols, providing a unified interface for interacting with various blockchain networks and protocols.
Plugin Architecture Overview
The edwin SDK uses a plugin-based architecture to provide a modular and extensible system for protocol integrations. Each plugin:
Extends the
EdwinPlugin
base classProvides tools for specific protocols
Defines parameter schemas using Zod
Implements protocol-specific functionality in service classes
The main components of the plugin architecture are:
EdwinPlugin: Base class for all protocol plugins
EdwinTool: Interface for tools exposed by plugins
EdwinService: Base class for services that implement protocol interactions
Parameter Schemas: Zod schemas that define the input parameters for tools
Directory and File Structure
When creating a new plugin, you should follow this directory and file structure:
src/
βββ plugins/
βββ yourplugin/
βββ index.ts # Exports the plugin
βββ parameters.ts # Parameter schemas and types
βββ yourPluginPlugin.ts # Plugin class implementation
βββ yourPluginService.ts # Protocol service implementation
βββ utils.ts # Optional utility functions
Creating a New Plugin
Step 1: Create the Plugin Directory
First, create a new directory for your plugin in the src/plugins
directory:
mkdir -p src/plugins/yourplugin
Step 2: Define Parameter Schemas
Create a parameters.ts
file to define the parameter schemas for your plugin's tools using Zod:
// src/plugins/yourplugin/parameters.ts
import { z } from 'zod';
import { createParameterSchema } from '../../core/utils/createParameterSchema';
// Define parameter schemas with validation and descriptions
export const YourOperationParametersSchema = createParameterSchema(
z.object({
chain: z.string().min(1).describe('The blockchain network to use'),
asset: z.string().min(1).describe('The asset to interact with'),
amount: z.number().positive().describe('The amount to use in the operation'),
// Add more parameters as needed
})
);
// Export clean parameter types without the "Schema" suffix
export type YourOperationParameters = typeof YourOperationParametersSchema.type;
Step 3: Implement the Protocol Service
Create a service class that extends EdwinService
to implement the protocol-specific functionality:
// src/plugins/yourplugin/yourPluginService.ts
import { EdwinService } from '../../core/classes/edwinToolProvider';
import { EdwinEVMWallet } from '../../core/wallets'; // Or EdwinSolanaWallet
import edwinLogger from '../../utils/logger';
import { YourOperationParameters } from './parameters';
import { withRetry } from '../../utils';
export class YourPluginService extends EdwinService {
private wallet: EdwinEVMWallet; // Or EdwinSolanaWallet
constructor(wallet: EdwinEVMWallet) {
// Or EdwinSolanaWallet
super();
this.wallet = wallet;
}
async yourOperation(params: YourOperationParameters): Promise<any> {
const { chain, asset, amount } = params;
edwinLogger.info(`Calling YourPlugin to perform operation with ${amount} ${asset} on ${chain}`);
try {
// Implement your protocol-specific logic here
// Use this.wallet to interact with the blockchain
// Example: Using withRetry for blockchain operations
const result = await withRetry(async () => {
// Your operation implementation
return { success: true, txHash: '0x...' };
}, 'YourPlugin operation');
return result;
} catch (error: unknown) {
edwinLogger.error('YourPlugin operation error:', error);
const message = error instanceof Error ? error.message : String(error);
throw new Error(`YourPlugin operation failed: ${message}`);
}
}
}
Step 4: Create the Plugin Class
Create a plugin class that extends EdwinPlugin
to define the tools your plugin provides:
// src/plugins/yourplugin/yourPluginPlugin.ts
import { EdwinPlugin } from '../../core/classes/edwinPlugin';
import { EdwinTool, Chain } from '../../core/types';
import { YourPluginService } from './yourPluginService';
import { EdwinEVMWallet } from '../../core/wallets'; // Or EdwinSolanaWallet
import { YourOperationParameters, YourOperationParametersSchema } from './parameters';
export class YourPlugin extends EdwinPlugin {
constructor(wallet: EdwinEVMWallet) {
// Or EdwinSolanaWallet
super('yourplugin', [new YourPluginService(wallet)]);
}
getTools(): Record<string, EdwinTool> {
const yourPluginService = this.toolProviders.find(
provider => provider instanceof YourPluginService
) as YourPluginService;
return {
yourPluginOperation: {
name: 'yourplugin_operation',
description: 'Perform an operation using YourPlugin',
schema: YourOperationParametersSchema.schema,
execute: async (params: YourOperationParameters) => {
return await yourPluginService.yourOperation(params);
},
},
// Add more tools as needed
};
}
// Define which chains your plugin supports
supportsChain = (chain: Chain) => ['ethereum', 'base', 'avalanche'].includes(chain.name); // For EVM chains
// Or for Solana: supportsChain = (chain: Chain) => chain.name === 'solana';
}
// Factory function to create a new instance of your plugin
export const yourPlugin = (wallet: EdwinEVMWallet) => new YourPlugin(wallet); // Or EdwinSolanaWallet
Step 5: Export the Plugin
Create an index.ts
file to export your plugin:
// src/plugins/yourplugin/index.ts
export * from './yourPluginPlugin';
export * from './yourPluginService';
Then, update the main plugins index file to include your plugin:
// src/plugins/index.ts
// Add your plugin to the exports
export * from './yourplugin';
Testing Your Plugin
Testing is a crucial part of plugin development. Create test files for your plugin in the tests
directory:
tests/
βββ yourplugin.test.ts
βββ yourpluginService.test.ts
Writing Tests
Here's an example of how to write tests for your plugin:
// tests/yourplugin.test.ts
import { describe, it, expect } from 'vitest';
import { YourPluginService } from '../src/plugins/yourplugin/yourPluginService';
import { EdwinEVMWallet } from '../src/core/wallets'; // Or EdwinSolanaWallet
describe('YourPluginService', () => {
let service: YourPluginService;
let wallet: EdwinEVMWallet;
beforeEach(() => {
// Use a real wallet with a test private key
wallet = new EdwinEVMWallet(process.env.TEST_PRIVATE_KEY as `0x${string}`);
service = new YourPluginService(wallet);
});
it('should perform an operation successfully', async () => {
// Arrange
const params = {
chain: 'ethereum',
asset: 'eth',
amount: 1.0,
};
// Act
const result = await service.yourOperation(params);
// Assert
expect(result).toBeDefined();
expect(result.success).toBe(true);
});
// Add more tests as needed
});
Running Tests
To run tests for your plugin, use the following command:
pnpm test
For testing specific files:
pnpm test tests/plugins/yourplugin
Testing Best Practices
When implementing tests for DeFi protocol integrations:
Maintain two types of tests:
Mock tests that use test doubles for reliable CI/CD pipeline execution
Integration tests that use properly funded wallets to verify actual protocol interactions
Never replace or remove actual protocol implementations just to make tests pass. Tests should be fixed by:
Properly configuring test environments with required wallet keys and balances
Adding supplementary mocks where needed while preserving core implementation logic
Using test doubles only for external API calls and network interactions
Maintaining the integrity of protocol-specific business logic
Keep mock implementations strictly in test files and never add them to the SDK source files themselves, even when using environment-based conditionals.
Integrating with the edwin Client
To make your plugin accessible from the edwin client, update the src/client/edwin.ts
file:
Import your plugin:
// src/client/edwin.ts
import { yourPlugin, YourPlugin } from '../plugins/yourplugin';
Add your plugin to the
EdwinPlugins
interface:
// src/client/edwin.ts
interface EdwinPlugins {
// Existing plugins
yourplugin?: YourPlugin;
}
Initialize your plugin in the constructor:
// src/client/edwin.ts
constructor(config: EdwinConfig) {
// Initialize wallets
if (config.evmPrivateKey) {
this.wallets.evm = new EdwinEVMWallet(config.evmPrivateKey);
}
// ...
// Initialize plugins
if (this.wallets.evm) {
// Existing plugins
this.plugins.yourplugin = yourPlugin(this.wallets.evm);
}
// ...
}
Best Practices
Error Handling: Always use proper error handling and logging in your plugin.
Type Safety: Use TypeScript types and Zod schemas for type safety.
Retry Mechanism: Use the
withRetry
utility for blockchain operations that might fail.Logging: Use the
edwinLogger
for consistent logging across the SDK.Parameter Validation: Validate all input parameters using Zod schemas.
Chain Support: Clearly define which chains your plugin supports.
Documentation: Add clear descriptions to your tools and parameters.
Testing: Write comprehensive tests for your plugin.
Troubleshooting
Common Issues
Plugin not loading: Ensure your plugin is properly exported and imported in the edwin client.
Parameter validation errors: Check your Zod schemas for correct validation rules.
Wallet connection issues: Verify that the wallet is properly initialized and connected.
Transaction failures: Use the
withRetry
utility and proper error handling.
Debugging Tips
Use
edwinLogger.debug()
for detailed logging during development.Check the transaction simulation before sending it to the blockchain.
Verify wallet balances and permissions before performing operations.
Test your plugin with mock implementations before using real blockchain interactions.
By following this guide, you should be able to create a new plugin for the edwin SDK that integrates seamlessly with the existing architecture. If you encounter any issues or have questions, please refer to the edwin SDK documentation, join the edwin Discord community for support and discussions, or open an issue on the GitHub repository.
Last updated