Plugins

Overview

OpenZeppelin Relayer supports plugins to extend the functionality of the relayer.

Plugins are TypeScript functions running in the Relayer server that can include any arbitrary logic defined by the Relayer operator.

The plugin system features: - Handler Pattern: Simple export-based plugin development - TypeScript Support: Full type safety and IntelliSense - Plugin API: Clean interface for interacting with relayers - Docker Integration: Seamless development and deployment - Comprehensive Error Handling: Detailed logging and debugging capabilities

Configuration

Writing a Plugin

Plugins are declared under plugins directory, and are expected to be TypeScript files (.ts extension).

openzeppelin-relayer/
├── plugins/
│   └── my-plugin.ts    # Plugin code
└── config/
    └── config.json     # Plugins in configuration file

This approach uses a simple handler export pattern:

/// Required imports.
import { Speed, PluginAPI } from "@openzeppelin/relayer-sdk";

/// Define your plugin parameters interface
type MyPluginParams = {
  destinationAddress: string;
  amount?: number;
  message?: string;
  relayerId?: string;
}

/// Define your plugin return type
type MyPluginResult = {
  success: boolean;
  transactionId: string;
  message: string;
}

/// Export a handler function - that's it!
export async function handler(api: PluginAPI, params: MyPluginParams): Promise<MyPluginResult> {
    console.info("🚀 Plugin started...");

    // Validate parameters
    if (!params.destinationAddress) {
        throw new Error("destinationAddress is required");
    }

    // Use the relayer API
    const relayer = api.useRelayer(params.relayerId || "my-relayer");

    const result = await relayer.sendTransaction({
        to: params.destinationAddress,
        value: params.amount || 1,
        data: "0x",
        gas_limit: 21000,
        speed: Speed.FAST,
    });

    console.info(`Transaction submitted: ${result.id}`);

    // Wait for confirmation
    await result.wait({
        interval: 5000,  // Check every 5 seconds
        timeout: 120000  // Timeout after 2 minutes
    });

    return {
        success: true,
        transactionId: result.id,
        message: `Successfully sent ${params.amount || 1} wei to ${params.destinationAddress}`
    };
}

Legacy Pattern (Deprecated, but supported)

The runPlugin() pattern is deprecated and will be removed in a future version. Please migrate to the handler export pattern above. Legacy plugins will continue to work but will show deprecation warnings.
import { runPlugin, PluginAPI } from "./lib/plugin";

async function myPlugin(api: PluginAPI, params: any) {
    // Plugin logic here
    return "result";
}

runPlugin(myPlugin); // ⚠️ Deprecated - shows warning but still works

Example legacy plugin (plugins/examples/example-deprecated.ts):

import { PluginAPI, runPlugin } from "../lib/plugin";
import { Speed } from "@openzeppelin/relayer-sdk";

type Params = {
    destinationAddress: string;
};

async function example(api: PluginAPI, params: Params): Promise<string> {
    console.info("Plugin started...");

    const relayer = api.useRelayer("sepolia-example");
    const result = await relayer.sendTransaction({
        to: params.destinationAddress,
        value: 1,
        data: "0x",
        gas_limit: 21000,
        speed: Speed.FAST,
    });

    await result.wait();
    return "done!";
}

runPlugin(example);

Declaring in config file

Plugins are configured in the ./config/config.json file, under the plugins key.

The file contains a list of plugins, each with an id, path and timeout in seconds (optional).

The plugin path is relative to the /plugins directory

Example:

"plugins": [
  {
    "id": "my-plugin",
    "path": "my-plugin.ts",
    "timeout": 30
  }
]

Timeout

The timeout is the maximum time in seconds that the plugin can run. If the plugin exceeds the timeout, it will be terminated with an error.

The timeout is optional, and if not provided, the default is 300 seconds (5 minutes).

Plugin Development Guidelines

TypeScript Best Practices

  • Define Parameter Types: Always create interfaces or types for your plugin parameters

  • Define Return Types: Specify what your plugin returns for better developer experience

  • Handle Errors Gracefully: Use try-catch blocks and return structured error responses

  • Validate Input: Check required parameters and provide meaningful error messages

  • Use Async/Await: Modern async patterns for better readability

Testing Your Plugin

You can test your handler function directly:

import { handler } from './my-plugin';

// Mock API for testing (in real scenarios, use proper mocking)
const mockApi = {
  useRelayer: (id: string) => ({
    sendTransaction: async (tx: any) => ({ id: "test-tx-123", wait: async () => {} })
  })
} as any;

const result = await handler(mockApi, {
  destinationAddress: "0x742d35Cc6640C21a1c7656d2c9C8F6bF5e7c3F8A",
  amount: 1000
});
console.log(result);

Invocation

Plugins are invoked by hitting the api/v1/plugins/{plugin-id}/call endpoint.

The endpoint accepts a POST request. Example post request body:

{
  "destinationAddress": "0x742d35Cc6640C21a1c7656d2c9C8F6bF5e7c3F8A",
  "amount": 1000000000000000,
  "message": "Hello from OpenZeppelin Relayer!"
}

The parameters are passed directly to your plugin’s handler function.

Debugging

When invoking a plugin, the response will include:

  • logs: The logs from the plugin execution.

  • return_value: The returned value of the plugin execution.

  • error: An error message if the plugin execution failed.

  • traces: A list of messages sent between the plugin and the Relayer instance. This includes all the payloads passed through the PluginAPI object.

Complete Example

  1. Plugin Code (plugins/example.ts):

import { Speed, PluginAPI } from "@openzeppelin/relayer-sdk";

type ExampleParams = {
  destinationAddress: string;
  amount?: number;
  message?: string;
}

type ExampleResult = {
  success: boolean;
  transactionId: string;
  transactionHash: string | null;
  message: string;
  timestamp: string;
}

export async function handler(api: PluginAPI, params: ExampleParams): Promise<ExampleResult> {
    console.info("🚀 Example plugin started");
    console.info(`📋 Parameters:`, JSON.stringify(params, null, 2));

    try {
        // Validate parameters
        if (!params.destinationAddress) {
            throw new Error("destinationAddress is required");
        }

        const amount = params.amount || 1;
        const message = params.message || "Hello from OpenZeppelin Relayer!";

        console.info(`💰 Sending ${amount} wei to ${params.destinationAddress}`);

        // Get relayer and send transaction
        const relayer = api.useRelayer("my-relayer");
        const result = await relayer.sendTransaction({
            to: params.destinationAddress,
            value: amount,
            data: "0x",
            gas_limit: 21000,
            speed: Speed.FAST,
        });

        console.info(`✅ Transaction submitted: ${result.id}`);

        // Wait for confirmation
        const confirmation = await result.wait({
            interval: 5000,
            timeout: 120000
        });

        console.info(`🎉 Transaction confirmed: ${confirmation.hash}`);

        return {
            success: true,
            transactionId: result.id,
            transactionHash: confirmation.hash || null,
            message: `Successfully sent ${amount} wei to ${params.destinationAddress}. ${message}`,
            timestamp: new Date().toISOString()
        };

    } catch (error) {
        console.error("❌ Plugin execution failed:", error);
        return {
            success: false,
            transactionId: "",
            transactionHash: null,
            message: `Plugin failed: ${(error as Error).message}`,
            timestamp: new Date().toISOString()
        };
    }
}
  1. Plugin Configuration (config/config.json):

{
  "plugins": [
    {
      "id": "example-plugin",
      "path": "example-plugin.ts",
      "timeout": 30
    }
  ]
}
  1. API Invocation:

curl -X POST http://localhost:8080/api/v1/plugins/example-plugin/call \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
  "destinationAddress": "0x742d35Cc6640C21a1c7656d2c9C8F6bF5e7c3F8A",
  "amount": 1000000000000000,
  "message": "Test transaction from plugin"
}'
  1. API Response:

{
  "success": true,
  "message": "Plugin called successfully",
  "logs": [
    {
      "level": "info",
      "message": "🚀 Example plugin started"
    },
    {
      "level": "info",
      "message": "💰 Sending 1000000000000000 wei to 0x742d35Cc6640C21a1c7656d2c9C8F6bF5e7c3F8A"
    },
    {
      "level": "info",
      "message": "✅ Transaction submitted: tx-123456"
    },
    {
      "level": "info",
      "message": "🎉 Transaction confirmed: 0xabc123..."
    }
  ],
  "return_value": {
    "success": true,
    "transactionId": "tx-123456",
    "transactionHash": "0xabc123def456...",
    "message": "Successfully sent 1000000000000000 wei to 0x742d35Cc6640C21a1c7656d2c9C8F6bF5e7c3F8A. Test transaction from plugin",
    "timestamp": "2024-01-15T10:30:00.000Z"
  },
  "error": "",
  "traces": [
    {
      "relayer_id": "my-relayer",
      "method": "sendTransaction",
      "payload": {
        "to": "0x742d35Cc6640C21a1c7656d2c9C8F6bF5e7c3F8A",
        "value": "1000000000000000",
        "data": "0x",
        "gas_limit": 21000,
        "speed": "fast"
      }
    }
  ]
}

Response Fields

  • logs: Terminal output from the plugin (console.log, console.error, etc.)

  • return_value: The value returned by your plugin’s handler function

  • error: Error message if the plugin execution failed

  • traces: Messages exchanged between the plugin and the Relayer instance via PluginAPI

Migration from Legacy Pattern

Current Status

  • Legacy plugins still work - No immediate action required

  • ⚠️ Deprecation warnings - Legacy plugins will show console warnings

  • 📅 Future removal - The runPlugin pattern will be removed in a future major version

  • 🎯 Recommended action - Migrate to handler pattern for new plugins

Migration Steps

If you have existing plugins using runPlugin(), migration is simple:

Before (Legacy - still works):

import { runPlugin, PluginAPI } from "./lib/plugin";

async function myPlugin(api: PluginAPI, params: any): Promise<any> {
    // Your plugin logic
    return result;
}

runPlugin(myPlugin); // ⚠️ Shows deprecation warning

After (Modern - recommended):

import { PluginAPI } from "@openzeppelin/relayer-sdk";


export async function handler(api: PluginAPI, params: any): Promise<any> {
    // Same plugin logic - just export as handler!
    return result;
}

Step-by-Step Migration

  1. Remove the runPlugin() call at the bottom of your file

  2. Rename your function to handler (or create a new handler export)

  3. Export the handler function using export async function handler

  4. Add proper TypeScript types for better development experience

  5. Test your plugin to ensure it works with the new pattern

  6. Update your documentation to reflect the new pattern

Backwards Compatibility

The relayer will automatically detect which pattern your plugin uses:

  • If it finds a handler export → uses modern pattern

  • If no handler but runPlugin() was called → uses legacy pattern with warning

  • If neither → shows clear error message

This ensures a smooth transition period where both patterns work simultaneously.