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 class
Provides 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:
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';
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 , join the for support and discussions, or open an issue on the GitHub repository.