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 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:

  1. EdwinPlugin: Base class for all protocol plugins

  2. EdwinTool: Interface for tools exposed by plugins

  3. EdwinService: Base class for services that implement protocol interactions

  4. 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:

  1. 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

  2. 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

  3. 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:

  1. Import your plugin:

// src/client/edwin.ts
import { yourPlugin, YourPlugin } from '../plugins/yourplugin';
  1. Add your plugin to the EdwinPlugins interface:

// src/client/edwin.ts
interface EdwinPlugins {
    // Existing plugins
    yourplugin?: YourPlugin;
}
  1. 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

  1. Error Handling: Always use proper error handling and logging in your plugin.

  2. Type Safety: Use TypeScript types and Zod schemas for type safety.

  3. Retry Mechanism: Use the withRetry utility for blockchain operations that might fail.

  4. Logging: Use the edwinLogger for consistent logging across the SDK.

  5. Parameter Validation: Validate all input parameters using Zod schemas.

  6. Chain Support: Clearly define which chains your plugin supports.

  7. Documentation: Add clear descriptions to your tools and parameters.

  8. Testing: Write comprehensive tests for your plugin.

Troubleshooting

Common Issues

  1. Plugin not loading: Ensure your plugin is properly exported and imported in the edwin client.

  2. Parameter validation errors: Check your Zod schemas for correct validation rules.

  3. Wallet connection issues: Verify that the wallet is properly initialized and connected.

  4. Transaction failures: Use the withRetry utility and proper error handling.

Debugging Tips

  1. Use edwinLogger.debug() for detailed logging during development.

  2. Check the transaction simulation before sending it to the blockchain.

  3. Verify wallet balances and permissions before performing operations.

  4. 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