Actions

Actions 允许你为链上和链下操作实现自定义的应用逻辑。你可以启用对 MonitorWorkflows 模块检测到的威胁的自动响应。

使用场景

  • 自动化智能合约操作

  • 执行操作以响应 Monitor 警报

  • 使用 Actions 自动化 Workflow 步骤

  • 调用外部 API 并与其他智能合约交互

Actions

Actions 是自动化的 JavaScript 代码段,可以通过触发器执行。

自动操作

触发器

支持以下触发器:

  • 计划: 选择一个频率,Defender 将按指定的间隔调用该函数。请注意,指定的间隔是两个连续执行开始之间的时间间隔,_而不是_一个运行结束和下一个运行开始之间的时间间隔。或者,可以使用 cron 表达式 指定操作的运行时间。

  • Webhook: Defender 将为该操作创建一个 webhook URL,每当向该端点发送 HTTP POST 请求时,该 URL 将被执行。该 URL 可以随时重新生成。调用后,HTTP 请求信息将被注入到操作中,并且 HTTP 响应将包括操作运行信息以及从操作返回的任何数据。

重要提示: 当向 webhook 发送请求时,请确保包含 Content-Type: application/json 头部。我们严格要求请求中必须存在这个头部。否则,你将看到 415 Unsupported Media Type 错误。

  • Monitor: 由 Defender Monitor 触发。它将包含一个 body 属性,其中包含触发事件的详细信息,你可以使用这些信息来运行自定义逻辑。

环境

Actions 在一个具有 256mb RAM 和 5 分钟超时时间的 node 20 环境中运行。每个操作的代码大小必须小于 5mb。为了便于使用,环境中预安装了一组常用依赖项。最新的 action 依赖版本 v2025-01-16 具有以下依赖项:

"@datadog/datadog-api-client": "^1.0.0-beta.5",
"@fireblocks/fireblocks-web3-provider": "^1.3.1",
"@gnosis.pm/safe-core-sdk": "^0.3.1",
"@gnosis.pm/safe-ethers-adapters": "^0.1.0-alpha.3",
"@openzeppelin/defender-admin-client": "1.54.6",
"@openzeppelin/defender-autotask-client": "1.54.6",
"@openzeppelin/defender-autotask-utils": "1.54.6",
"@openzeppelin/defender-kvstore-client": "1.54.6",
"@openzeppelin/defender-relay-client": "1.54.6",
"@openzeppelin/defender-sdk": "1.15.2",
"@openzeppelin/defender-sentinel-client": "1.54.6",
"axios": "^1.7.4",
"axios-retry": "3.5.0",
"ethers": "5.5.3",
"fireblocks-sdk": "^2.5.4",
"graphql": "^15.5.1",
"graphql-request": "3.4.0",
"web3": "1.9.0"
不在 @openzeppelin 命名空间下的 Defender 依赖项现在已被弃用。受影响的依赖项包括 defender-admin-client,defender-autotask-client,defender-autotask-utils,defender-kvstore-client,defender-relay-client,defender-sentinel-client
如果需要其他依赖项,则可以使用 JavaScript 模块打包器,例如 rollup 或 webpack。参考 这个示例项目 了解如何操作。请与我们联系,添加你认为其他用户会觉得有用的依赖项!
我们 OpenZeppelin 开发团队非常喜欢 Typescript,我们也希望你也喜欢!如果你想用 TypeScript 编写你的 actions,你需要首先使用 tsc 或你选择的打包器编译它们,然后上传生成的 JavaScript 代码。不幸的是,我们不支持在用户界面中直接使用 TypeScript 编码。所有 defender-sdk 包都是用 TypeScript 编写的,并且打包了它们的类型声明。你也可以使用 openzeppelin/defender-sdk-action-client 包来获取事件负载的类型定义。

运行时升级

Actions 必须与最新的 Node.js 运行时版本保持同步,以确保它们在最新的安全环境中运行。有时,我们会强制对所有 Actions 进行运行时升级,以达到最低运行时版本。该过程包括:

  1. 在自动升级之前发送电子邮件通知。

  2. 在 actions 页面下显示 UI 横幅,以警告即将进行的自动升级。

  3. 发布论坛公告。

  4. 在自动升级当天,Defender 会自动将所有 action 运行时升级到最低要求的版本。

我们建议你检查你的 Actions,进行必要的更改,并提前升级,以防止任何重大更改。

定义代码

处理函数

你的代码必须导出一个异步 handler 函数,该函数将在每次执行 action 时被调用。

exports.handler = async function(event) {
  // Your code here
  // 你的代码
}

以下接口包含 Defender 在调用 Action 时注入的 event 类型:

export interface ActionEvent {
   /**
   * Internal identifier of the relayer function used by the relay-client
   */
   relayerARN?: string;

   /**
   * Internal identifier of the key-value store function used by the kvstore-client
   */
   kvstoreARN?: string;

   /**
   * Internal credentials generated by Defender for communicating with other services
   */
   credentials?: string;

   /**
   * Read-only key-value secrets defined in the Action secrets vault
   */
   secrets?: ActionSecretsMap;

   /**
   * Contains a Webhook request, Monitor match information, or Monitor match request
   */
   request?: ActionRequestData;
   /**
   * actionId is the unique identifier of the Action
   */
   actionId: string;
   /**
   * Name assigned to the Action
   */
   actionName: string;
   /**
   * Id of the the current Action run
   */
   actionRunId: string;
   /**
   * Previous Action run information
   */
   previousRun?: PreviousActionRunInfo;
}

Relayer 集成

如果将你的自动 action 连接到 relayer,则 Defender 将自动注入临时凭据,以便从 action 代码访问 relayer。只需将事件对象传递给 relayer 客户端,以代替凭据:

const { Defender } = require('@openzeppelin/defender-sdk');

exports.handler = async function(event) {
  const client = new Defender(event);

  // Use relayer for sending txs or querying the network...
  // 使用 relayer 发送交易或查询网络...
}

这允许你从 actions 发送交易,而无需设置任何 API 密钥或密钥。此外,你还可以使用 relayer 的 JSON RPC 端点来查询任何 Ethereum 网络,而无需为外部网络提供商配置 API 密钥。

我们还支持 ethers.js 通过 relayer 进行查询或发送交易。要使用 ethers.js,请将以上代码段替换为以下代码:

const { DefenderRelaySigner, DefenderRelayProvider } = require('defender-relay-client/lib/ethers');
const ethers = require('ethers');

exports.handler = async function(event) {
  const provider = new DefenderRelayProvider(event);
  const signer = new DefenderRelaySigner(event, provider, { speed: 'fast' });
  // Use provider and signer for querying or sending txs from ethers, for example...
  // 使用 provider 和 signer 从 ethers 进行查询或发送交易,例如...
  const contract = new ethers.Contract(ADDRESS, ABI, signer);
  await contract.ping();
}

如果你喜欢 web3.js:

const { DefenderRelayProvider } = require('defender-relay-client/lib/web3');
const Web3 = require('web3');

exports.handler = async function(event) {
  const provider = new DefenderRelayProvider(event, { speed: 'fast' });
  const web3 = new Web3(provider);
  // Use web3 instance for querying or sending txs, for example...
  // 使用 web3 实例进行查询或发送交易,例如...
  const [from] = await web3.eth.getAccounts();
  const contract = new web3.eth.Contract(ABI, ADDRESS, { from });
  await contract.methods.ping().send();
}

Monitor 调用

从 Monitor 触发的 Actions 可以有两种类型的 body 属性和 scheme,具体取决于触发 action 的 Monitor 类型:

如果 action 是用 TypeScript 编写的,则可以使用 defender-sdk-action-client 包中的 BlockTriggerEvent 类型。

exports.handler = async function(params) {
  const payload = params.request.body;
  const matchReasons = payload.matchReasons;
  const sentinel = payload.sentinel;

  // if contract monitor
  // 如果是合约 monitor
  const transaction  = payload.transaction;
  const abi = sentinel.abi;

  // custom logic...
  // 自定义逻辑...
}

Webhook 调用

当通过 webhook 调用 action 时,它可以访问 HTTP 请求信息,作为注入到 handler 中的 event 参数的一部分。同样,返回值将包含在 HTTP 响应负载的 result 字段中。

exports.handler = async function(event) {
  const {
    body,    // Object with JSON-parsed POST body
              // 具有 JSON 解析的 POST body 的对象
    headers, // Object with key-values from HTTP headers
              // 具有来自 HTTP header 的键值的对象
    queryParameters, // Object with key-values from query parameters
              // 具有来自 query parameters 的键值的对象
  } = event.request;

  return {
    hello: 'world' // JSON-serialized and included in the `result` field of the response
                   // JSON 序列化并包含在响应的 `result` 字段中
  };
}

目前仅支持 JSON 负载,并且仅将带有 X-Stripe- 前缀的非标准 header 提供给 action。

来自 webhook 端点的示例响应如下所示,其中 statussuccesserror 之一,encodedLogs 具有来自运行的 base64 编码的日志,result 具有从执行返回的 JSON 编码值。

{
  "autotaskRunId": "37a91eba-9a6a-4404-95e4-38d178ba69ed",
  "autotaskId": "19ef0257-bba4-4723-a18f-67d96726213e",
  "trigger": "webhook",
  "status": "success",
  "createdAt": "2021-02-23T18:49:14.812Z",
  "encodedLogs": "U1RBU...cwkK",
  "result": "{\"hello\":\"world\"}",
  "requestId": "e7979150-44d3-4021-926c-9d9679788eb8"
}
超过 25 秒才能完成的 Actions 将返回具有挂起状态的响应。但是,action 将继续在后台运行并最终完成(在 5 分钟内)。
如果 {"message":"Missing Authentication Token"} 是对 Webhook HTTP 请求的响应,请仔细检查该请求实际上是否为 POST。发出 GET 请求时会发生此响应。
Webhook 请求具有严格的 content-type 要求。如果请求没有 Content-Type: application/json 头部,则 action 调用将返回 415 Unsupported Media Type 错误。请确保在你的请求中包含此头部。

密钥

Defender 密钥允许你存储敏感信息,例如可以从 actions 安全访问的 API 密钥和密钥。+ Action 密钥是字符串的键值区分大小写对,可以使用 event.secrets 对象从 action 代码访问。action 使用的密钥数量没有限制。密钥在所有 actions 之间共享,而不是特定于单个 action。

exports.handler = async function(event) {
  const { mySecret, anApiKey } = event.secrets;
}

密钥经过加密并存储在安全存储库中,仅在 action 运行时解密以进行注入。写入后,密钥只能从用户界面删除或覆盖,而不能读取。

action 可能会记录密钥的值,从而意外地泄漏它。
虽然可以使用密钥来存储用于签署消息或交易的私钥,但我们建议改用 Defender relayer。与在 action 代码中加载私钥并在那里签名相比,Defender relayers 的签名操作提供了更高的安全性。

键值数据存储

action 键值数据存储允许在 action 运行之间以及不同的 actions 之间持久保存简单数据。它可以用于存储交易标识符、哈希的用户电子邮件,甚至小型序列化对象。

你可以通过 Defender 实例与你的键值存储进行交互,该实例使用注入到你的 Action handler 函数中的负载进行初始化。初始化后,你可以调用 kvstore.getkvstore.putkvstore.del

const { Defender } = require('@openzeppelin/defender-sdk');

exports.handler = async function (event) {
  const client = new Defender(event);

  await client.keyValueStore.put('myKey', 'myValue');
  const value = await client.keyValueStore.get('myKey');
  await client.keyValueStore.del('myKey');
};

键值存储允许获取、放置和删除键值对,这些键值对必须是限制为 1 KB 的字符串,值限制为 300 KB。

存储的数据在所有 actions 之间共享。为了隔离每个 action 管理的记录,建议使用每个 action 唯一的命名空间为键添加前缀。
每个项目在上次更新后 90 天到期。如果需要长期存在的数据存储,我们建议设置一个外部数据库,并使用 action 密钥来存储连接到它的凭据。

通知

Actions 可以通过 Defender 通知设置中已定义的各种渠道发送通知。此集成允许你快速通知其他连接的系统关于 actions 检测或进行的更改。

要发送通知,你应该使用 notificationClient.send(),如以下示例所示:

exports.handler = async function(credentials, context) {
  const { notificationClient } = context;

  try {
    notificationClient.send({
      channelAlias: 'example-email-notification-channel-alias',
      subject: 'Action notification example',
      message: 'This is an example of a email notification sent from an action',
    });
  } catch (error) {
    console.error('Failed to send notification', error);
  }
}

对于电子邮件通知,支持基本的 HTML 标签。以下是如何生成 HTML 消息的示例:

function generateHtmlMessage(actionName, txHash) {
  return `
<h1>Transaction sent from Action ${actionName}</h1>
<p>Transaction with hash <i>${txHash}</i> was sent.</p>
`;
}

exports.handler = async function(event, context) {
  const { notificationClient } = context;

  const relayer = new Relayer(credentials);

  const txRes = await relayer.sendTransaction({
    to: '0xc7464dbcA260A8faF033460622B23467Df5AEA42',
    value: 100,
    speed: 'fast',
    gasLimit: '21000',
  });

  try {
    notificationClient.send({
      channelAlias: 'example-email-notification-channel-alias',
      subject: `Transaction sent from Action ${event.actionName}`,
      message: generateHtmlMessage(event.actionName, txRes.hash),
    });
  } catch (error) {
    console.error('Failed to send notification', error);
  }
}

要发送指标通知,请改用 notificationClient.sendMetric() 方法,如以下示例所示:

exports.handler = async function(credentials, context) {
  const { notificationClient } = context;

  try {
    notificationClient.sendMetric({
      channelAlias: 'example-email-notification-channel-alias',
      name: 'datadog-test-metric',
      value: 1,
    });
  } catch (error) {
    console.error('Failed to send notification', error);
  }
}
如果传递了无效或暂停的 notification channelAlias,则会抛出错误。
如果由于任何其他原因无法发送通知,则不会抛出错误,但会将状态消息添加到 action 日志中。例如,如果发送到具有不活动 URL 的 webhook 渠道的通知,则会添加日志条目,但不会抛出错误。
如果多个通知渠道使用相同的别名,则通知将发送给所有这些渠道。

错误处理

导致错误的自动 action 调用在 action 运行响应中包含一个 errorType 字段,该字段将设置为 defender-sdk 中定义的 ActionErrorType。用户可读的错误也会出现在“运行历史记录”视图中。

本地开发

如果想在本地重现 action 的行为以进行调试或测试,请按照下列步骤操作:

  • 初始化一个新的 npm 项目 (npm init)

  • package.json 中的 dependencies 键设置为上面 Environment 部分中指示的包

  • 下载 yarn.lock: 📎 yarn.lock

  • 运行 yarn install --frozen-lockfile

你还可以使用以下模板进行本地开发,该模板将在使用 node 调用时运行 action 代码。它将从环境变量加载 relayer 凭据,或者在使用 Defender 运行时使用注入的凭据。

const { Defender } = require('@openzeppelin/defender-sdk');


// Entrypoint for the action
// action 的入口点
exports.handler = async function(event) {
  const client = new Defender(credentials);
  // Use client.relaySigner for sending txs
  // 使用 client.relaySigner 发送 txs
}

// To run locally (this code will not be executed in actions)
// 在本地运行(此代码不会在 actions 中执行)
if (require.main === module) {
  const { RELAYER_API_KEY: apiKey, RELAYER_API_SECRET: apiSecret } = process.env;
  exports.handler({ apiKey, apiSecret })
    .then(() => process.exit(0))
    .catch(error => { console.error(error); process.exit(1); });
}

请记住在 event 对象中发送你的 action 期望的任何其他值,例如密钥或 monitor 事件。

更新代码

你可以通过 Defender 界面或使用 defender-sdk npm 包通过 API 编程方式编辑 action 的代码。后者允许上传包含多个文件的代码包:

代码包在压缩和 base64 编码后的大小不得超过 5MB,并且必须始终在 zip 文件的根目录中包含一个 index.js 作为入口点。

Workflows

Workflows 允许你通过预定义的操作和场景立即检测、响应和解决威胁和攻击。你还可以进行攻击模拟并在分叉网络上测试真实场景。

Workflows 是结合了自动 actions 和事务模板的流程。可以并行运行 actions 或顺序连接 actions。可以通过 Monitor 手动或触发 Workflows。

创建 workflows 是一种无缝体验,通过一个表单引导,该表单允许你轻松地在 workflow 过程中组织 actions 。

创建 Workflow

要填充 workflow,你必须将右侧列表中的现有 actions 拖到表单上。Actions 垂直执行,这意味着先前的 actions 必须成功完成才能开始执行新行。并行 actions 同时执行。但是,如果某个 action 退出并出现错误,workflow 将完全停止。

编辑 Workflow

要并行运行多个 actions,请单击“添加并行序列”并将 actions 拖到可用的并排框中。

并行 Workflow

你可以将 actions 从 workflow 中拖回以删除它们,或者单击右上角可见的减号图标以删除空步骤。右上角的“保存”按钮使用其配置和名称保存 workflow。

我们提供了一个快速入门教程来创建和使用 Workflows。在此处查看 此处

一个完整的例子

以下示例使用 ethers.js 和 relayer 集成来发送调用给定合约上的 execute 的交易。在发送交易之前,它会检查 canExecute 视图函数,并验证通过 webhook 接收的参数是否与本地密钥匹配。如果发送了交易,它会在响应中返回哈希,该响应将发送回 webhook 调用者。

const { ethers } = require("ethers");
const { DefenderRelaySigner, DefenderRelayProvider } = require('defender-relay-client/lib/ethers');

// Entrypoint for the action
// action 的入口点
exports.handler = async function(event) {
  // Load value provided in the webhook payload (not available in schedule or sentinel invocations)
  // 加载 webhook 负载中提供的值(在计划或 sentinel 调用中不可用)
  const { value } = event.request.body;

  // Compare it with a local secret
  // 将其与本地密钥进行比较
  if (value !== event.secrets.expectedValue) return;

  // Initialize relayer provider and signer
  // 初始化 relayer 提供程序和签名者
  const provider = new DefenderRelayProvider(event);
  const signer = new DefenderRelaySigner(event, provider, { speed: 'fast' });

  // Create contract instance from the signer and use it to send a tx
  // 从签名者创建合约实例并使用它来发送交易
  const contract = new ethers.Contract(ADDRESS, ABI, signer);
  if (await contract.canExecute()) {
    const tx = await contract.execute();
    console.log(`Called execute in ${tx.hash}`);
    return { tx: tx.hash };
  }
}
代码不需要等待挖掘交易。Defender 将负责监视交易并在需要时重新提交。action 只需要发送请求并退出。

安全注意事项

每个 action 的代码都隔离在 Defender 中,并且 actions 受到严格的访问控制限制,无法访问其他 Defender 内部基础结构。唯一的例外是 action 可能会访问其链接的 relayer,这是通过 action 服务在每次执行时注入的临时凭据协商的。尽管如此,action 只能调用 relayer 的公开方法,并且无法直接访问后备私钥或任何其他服务。

我们提供了一个快速入门教程,用于使用 Defender 为智能合约创建自动 action。在此处查看 此处