OpenZeppelin Relayer 插件系统

本文档介绍了OpenZeppelin Relayer的插件系统,该系统允许开发者通过TypeScript函数扩展Relayer的功能。文章详细说明了插件的配置方式、开发指南、调用方法以及调试技巧,并提供了完整的示例。此外,还讨论了从旧模式迁移到推荐的handler模式的步骤和兼容性。

插件

概述

OpenZeppelin Relayer 支持插件以扩展 relayer 的功能。

插件是 TypeScript 函数,运行在 Relayer 服务器中,可以包含 Relayer 运营商定义的任何任意逻辑。

插件系统具有以下特点:

  • Handler 模式:基于简单导出的插件开发
  • TypeScript 支持:完整的类型安全和智能感知
  • Plugin API:用于与 relayers 交互的清晰接口
  • Docker 集成:无缝开发和部署
  • 全面的错误处理:详细的日志记录和调试功能

配置

编写插件

插件在 plugins 目录下声明,并且应该是 TypeScript 文件(.ts 扩展名)。

openzeppelin-relayer/
├── plugins/
│   └── my-plugin.ts    # 插件代码
└── config/
    └── config.json     # 配置文件中的插件
Handler 模式(推荐)

此方法使用简单的 handler 导出模式:

/// 必需的导入。
import { Speed, PluginAPI } from "@openzeppelin/relayer-sdk";

/// 定义你的插件参数接口
type MyPluginParams = {
  destinationAddress: string;
  amount?: number;
  message?: string;
  relayerId?: string;
}

/// 定义你的插件返回类型
type MyPluginResult = {
  success: boolean;
  transactionId: string;
  message: string;
}

/// 导出一个 handler 函数 - 就这样!
export async function handler(api: PluginAPI, params: MyPluginParams): Promise<MyPluginResult> {
    console.info("🚀 插件已启动...");

    // 验证参数
    if (!params.destinationAddress) {
        throw new Error("destinationAddress is required");
    }

    // 使用 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}`);

    // 等待确认
    await result.wait({
        interval: 5000,  // 每 5 秒检查一次
        timeout: 120000  // 2 分钟后超时
    });

    return {
        success: true,
        transactionId: result.id,
        message: `Successfully sent ${params.amount || 1} wei to ${params.destinationAddress}`
    };
}
传统模式(已弃用,但仍支持)
runPlugin() 模式已被弃用,并将在未来的版本中移除。请迁移到上面的 handler 导出模式。传统的插件将继续工作,但会显示弃用警告。
import { runPlugin, PluginAPI } from "./lib/plugin";

async function myPlugin(api: PluginAPI, params: any) {
    // 在这里编写插件逻辑
    return "result";
}

runPlugin(myPlugin); // ⚠️ 已弃用 - 显示警告但仍然有效

示例传统插件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("插件已启动...");

    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);

在配置文件中声明

插件在 ./config/config.json 文件中配置,位于 plugins 键下。

该文件包含一个插件列表,每个插件都有一个 id、路径和超时时间(以秒为单位)(可选)。

插件路径是相对于 /plugins 目录的

示例:

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

超时

超时是插件可以运行的最大时间(以秒为单位)。如果插件超过超时时间,它将终止并显示错误。

超时是可选的,如果未提供,则默认为 300 秒(5 分钟)。

插件开发指南

TypeScript 最佳实践

  • 定义参数类型:始终为插件参数创建接口或类型

  • 定义返回类型:指定插件返回的内容,以获得更好的开发体验

  • 优雅地处理错误:使用 try-catch 代码块并返回结构化的错误响应

  • 验证输入:检查必需的参数并提供有意义的错误消息

  • 使用 Async/Await:现代的异步模式,以获得更好的可读性

测试你的插件

你可以直接测试你的 handler 函数:

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

// 用于测试的模拟 API(在实际场景中,使用适当的模拟)
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);

调用

插件通过访问 api/v1/plugins/{plugin-id}/call 端点来调用。

该端点接受 POST 请求。示例 post 请求体:

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

这些参数直接传递到你的插件的 handler 函数中。

调试

当调用插件时,响应将包括:

  • logs:插件执行的日志。

  • return_value:插件执行的返回值。

  • error:如果插件执行失败,则显示错误消息。

  • traces:插件和 Relayer 实例之间发送的消息列表。这包括通过 PluginAPI 对象传递的所有 payload。

完整示例

  1. 插件代码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("🚀 示例插件已启动");
    console.info(`📋 参数:`, JSON.stringify(params, null, 2));

    try {
        // 验证参数
        if (!params.destinationAddress) {
            throw new Error("destinationAddress is required");
        }

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

        console.info(`💰 发送 ${amount} wei 到 ${params.destinationAddress}`);

        // 获取 relayer 并发送交易
        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(`✅ 交易已提交: ${result.id}`);

        // 等待确认
        const confirmation = await result.wait({
            interval: 5000,
            timeout: 120000
        });

        console.info(`🎉 交易已确认: ${confirmation.hash}`);

        return {
            success: true,
            transactionId: result.id,
            transactionHash: confirmation.hash || null,
            message: `已成功发送 ${amount} wei 到 ${params.destinationAddress}。${message}`,
            timestamp: new Date().toISOString()
        };

    } catch (error) {
        console.error("❌ 插件执行失败:", error);
        return {
            success: false,
            transactionId: "",
            transactionHash: null,
            message: `插件失败: ${(error as Error).message}`,
            timestamp: new Date().toISOString()
        };
    }
}
  1. 插件配置config/config.json):
{
  "plugins": [\
    {\
      "id": "example-plugin",\
      "path": "example-plugin.ts",\
      "timeout": 30\
    }\
  ]
}
  1. API 调用
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 响应
{
  "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"\
      }\
    }\
  ]
}

响应字段

  • logs:插件的终端输出(console.log、console.error 等)

  • return_value:插件的 handler 函数返回的值

  • error:如果插件执行失败,则显示错误消息

  • traces:通过 PluginAPI 在插件和Relayer 实例之间交换的消息

从传统模式迁移

当前状态

  • 传统插件仍然有效 - 无需立即采取措施

  • ⚠️ 弃用警告 - 传统插件将显示控制台警告

  • 📅 未来移除 - runPlugin 模式将在未来的主要版本中移除

  • 🎯 推荐操作 - 迁移到 handler 模式以开发新插件

迁移步骤

如果你有使用 runPlugin() 的现有插件,则迁移很简单:

之前(传统模式 - 仍然有效)

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

async function myPlugin(api: PluginAPI, params: any): Promise<any> {
    // 你的插件逻辑
    return result;
}

runPlugin(myPlugin); // ⚠️ 显示弃用警告

之后(现代模式 - 推荐)

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

export async function handler(api: PluginAPI, params: any): Promise<any> {
    // 相同的插件逻辑 - 只需导出为 handler!
    return result;
}

逐步迁移

  1. 删除文件底部的 runPlugin() 调用

  2. 将你的函数重命名为 handler(或创建一个新的 handler 导出)

  3. 使用 export async function handler 导出 handler 函数

  4. 添加适当的 TypeScript 类型,以获得更好的开发体验

  5. 测试你的插件,以确保它在新模式下工作

  6. 更新你的文档,以反映新的模式

向后兼容性

relayer 将自动检测你的插件使用哪种模式:

  • 如果它找到一个 handler 导出 → 使用现代模式

  • 如果没有 handler,但是调用了 runPlugin() → 使用带有警告的传统模式

  • 如果都没有 → 显示明确的错误消息

这确保了一个平稳的过渡期,其中两种模式可以同时工作。

  • 原文链接: docs.openzeppelin.com/re...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。