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
Handler Pattern (Recommended)
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
}
]
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 thePluginAPI
object.
Complete Example
-
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()
};
}
}
-
Plugin Configuration (
config/config.json
):
{
"plugins": [
{
"id": "example-plugin",
"path": "example-plugin.ts",
"timeout": 30
}
]
}
-
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"
}'
-
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
-
Remove the
runPlugin()
call at the bottom of your file -
Rename your function to
handler
(or create a new handler export) -
Export the
handler
function usingexport async function handler
-
Add proper TypeScript types for better development experience
-
Test your plugin to ensure it works with the new pattern
-
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
butrunPlugin()
was called → uses legacy pattern with warning -
If neither → shows clear error message
This ensures a smooth transition period where both patterns work simultaneously.