本文介绍了OpenZeppelin Defender中的Actions功能,该功能允许用户为链上和链下操作实现自定义的应用逻辑,可用于智能合约操作的自动化、响应Monitor警报、自动化Workflow步骤以及调用外部API等。
操作允许你为链上和链下操作实现自定义应用程序逻辑。你可以启用对监控器(Monitor)和工作流(Workflows)模块检测到的威胁的自动响应。
自动化智能合约操作
执行操作以响应监控器(Monitor)警报
使用操作自动化工作流(Workflow)步骤
调用外部 API 并与其他智能合约交互
操作是自动化的 Javascript 代码片段,可以通过触发器执行。
支持以下触发器:
计划(Schedule):选择一个频率,Defender 将按指定的间隔调用该函数。请注意,指定的间隔是两次连续执行开始之间的时间,不是一次运行结束和下一次运行开始之间的时间。或者,可以使用 cron 表达式 指定操作应何时运行。
Webhook:Defender 将为操作创建一个 webhook URL,每当向该端点发送 HTTP POST 请求时,该 URL 将被执行。该 URL 可以随时重新生成。调用时,HTTP 请求信息将被注入到操作中,并且 HTTP 响应将包括操作运行信息以及从操作返回的任何数据。
向 webhook 发送请求时,请务必包含 Content-Type: application/json 标头。我们严格要求请求中存在此标头。否则,你会看到 415 Unsupported Media Type 错误. |
操作在具有 256mb RAM 和 5 分钟超时的 node 20 environment 中运行。每个操作的代码大小必须小于 5mb。为了便于使用,环境中预安装了一组常用依赖项。最新的操作依赖项版本 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 编写你的操作,你需要首先使用 tsc 或你选择的打包器编译它们,然后上传生成的 JavaScript 代码。不幸的是,我们不支持在用户界面中直接用 TypeScript 编写代码。所有 defender-sdk 包都用 TypeScript 编写,并打包了它们的类型声明。你还可以使用 openzeppelin/defender-sdk-action-client 包来获取事件有效负载的类型定义。 |
必须使用最新的 Node.js 运行时版本更新操作,以确保它们在最新且安全的环境中运行。有时,我们会强制将所有操作的运行时升级到最低运行时版本。该过程包括:
在自动升级之前发送电子邮件通知。
在操作页面下显示一个 UI 横幅,警告即将进行的自动升级。
发布论坛公告。
在自动升级当天,Defender 会自动将所有操作运行时升级到最低要求的版本。
我们建议检查你的操作,进行必要的更改,并提前升级,以防止任何重大更改。 |
你的代码必须导出一个 async handler
函数,该函数将在每次执行操作时被调用。
exports.handler = async function(event) {
// 你的代码在这里
}
以下接口包含 Defender 在调用操作时注入的 event
类型:
export interface ActionEvent {
/**
* 继电器客户端使用的继电器功能的内部标识符
*/
relayerARN?: string;
/**
* kvstore-client 使用的键值存储功能的内部标识符
*/
kvstoreARN?: string;
/**
* Defender 生成的用于与其他服务通信的内部凭据
*/
credentials?: string;
/**
* 在 Action 秘密保管库中定义的只读键值秘密
*/
secrets?: ActionSecretsMap;
/**
* 包含 Webhook 请求、Monitor 匹配信息或 Monitor 匹配请求
*/
request?: ActionRequestData;
/**
* actionId 是 Action 的唯一标识符
*/
actionId: string;
/**
* 分配给 Action 的名称
*/
actionName: string;
/**
* 当前 Action 运行的 Id
*/
actionRunId: string;
/**
* 先前 Action 运行信息
*/
previousRun?: PreviousActionRunInfo;
}
如果将你的自动操作连接到继电器,那么 Defender 将自动注入临时凭据,以便从操作代码访问继电器。只需将 event 对象传递给继电器客户端,以代替凭据:
const { Defender } = require('@openzeppelin/defender-sdk');
exports.handler = async function(event) {
const client = new Defender(event);
// 使用继电器发送交易或查询网络...
}
这允许你使用来自操作的继电器发送交易,而无需设置任何 API 密钥或秘密。此外,你还可以使用继电器的 JSON RPC 端点来查询任何 Ethereum 网络,而无需为外部网络提供商配置 API 密钥。
我们还支持 ethers.js
用于通过继电器进行查询或发送交易。要使用 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' });
// 使用 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);
// 使用 web3 实例查询或发送交易,例如...
const [from] = await web3.eth.getAccounts();
const contract = new web3.eth.Contract(ABI, ADDRESS, { from });
await contract.methods.ping().send();
}
从监控器触发的操作可以有两种类型的 body 属性和模式,具体取决于触发操作的监控器的类型:
如果该操作是用 TypeScript 编写的,则可以使用 defender-sdk-action-client 包中的 BlockTriggerEvent
类型。
exports.handler = async function(params) {
const payload = params.request.body;
const matchReasons = payload.matchReasons;
const sentinel = payload.sentinel;
// 如果是合约监控器
const transaction = payload.transaction;
const abi = sentinel.abi;
// 自定义逻辑...
}
当通过 webhook 调用操作时,它可以访问作为 handler 中注入的 event
参数一部分的 HTTP 请求信息。同样,返回值将包含在 HTTP 响应有效负载的 result
字段中。
exports.handler = async function(event) {
const {
body, // 带有 JSON 解析的 POST body 的对象
headers, // 带有来自 HTTP 标头的键值对的对象
queryParameters, // 带有来自查询参数的键值对的对象
} = event.request;
return {
hello: 'world' // JSON 序列化并包含在响应的 `result` 字段中
};
}
目前仅支持 JSON 有效负载,并且仅向操作提供带有 X-
或 Stripe-
前缀的非标准标头。
来自 webhook 端点的示例响应如下所示,其中 status
是 success
或 error
之一,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 秒的操作将返回一个具有待处理状态的响应。尽管如此,该操作将继续在后台运行并最终完成(在 5 分钟内)。 |
如果 {"message":"Missing Authentication Token"} 是对 Webhook HTTP 请求的响应,请仔细检查该请求是否实际上是 POST 请求。当发出 GET 请求时会发生此响应. |
Webhook 请求具有严格的 content-type 要求。如果请求没有 Content-Type: application/json 标头,则操作调用将返回 415 Unsupported Media Type 错误。请确保在你的请求中包含此标头。 |
Defender 密钥允许你存储敏感信息,例如可以从操作安全访问的 API 密钥和密钥。
操作密钥是字符串的键值对(区分大小写),可以使用 event.secrets
对象从操作代码访问。操作使用的密钥数量没有限制。密钥在所有操作之间共享,而不是特定于单个操作的。
exports.handler = async function(event) {
const { mySecret, anApiKey } = event.secrets;
}
密钥经过加密并存储在安全保管库中,仅在操作运行时解密以进行注入。写入后,只能从用户界面删除或覆盖密钥,而不能读取。
操作可能会记录密钥的值,从而意外泄露它。 |
虽然可以使用密钥来存储用于签名消息或交易的私钥,但我们建议改用 Defender 继电器。Defender 继电器的签名操作比在操作代码中加载私钥并在那里签名提供更高的安全性。 |
操作键值数据存储允许跨操作运行和不同操作之间持久保存简单数据。它可用于存储交易标识符、哈希的用户电子邮件,甚至小型序列化对象。
你可以通过 Defender
实例与键值存储进行交互,该实例使用注入到 Action handler
函数中的有效负载进行初始化。初始化后,你可以调用 kvstore.get
、kvstore.put
或 kvstore.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。
存储的数据会在所有操作之间共享。为了隔离每个操作管理的记录,建议使用每个操作独有的命名空间对键进行前缀。 |
每个项目在上次更新后 90 天过期。如果需要长期存在的数据存储,我们建议设置外部数据库并使用操作密钥来存储连接到它的凭据。 |
操作可以通过 Defender 通知设置中已定义的各种渠道发送通知。此集成允许你快速通知其他连接的系统有关操作检测到或进行的更改。
要发送通知,你应该使用 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,则会抛出错误。 |
如果由于任何其他原因无法发送通知,则不会抛出错误,但状态消息将添加到操作日志中。例如,如果发送到具有非活动 URL 的 webhook 渠道的通知,则将添加日志条目,但不会抛出错误。 |
如果多个通知渠道使用相同的别名,则会将通知发送到所有这些渠道。 |
导致错误的自动操作调用在操作运行响应中包含一个 errorType
字段,该字段将设置为 ActionErrorType,如 defender-sdk 中定义。用户可读的错误也会出现在“运行历史记录”视图中。
如果你想在本地重现操作的行为以进行调试或测试,请按照以下步骤操作:
初始化一个新的 npm 项目 ( npm init
)
将 package.json
中的 dependencies
键设置为上面 Environment 部分中指示的包
下载 yarn.lock
:📎 yarn.lock
运行 yarn install --frozen-lockfile
。
你还可以使用以下模板进行本地开发,该模板将在使用 node
调用时运行操作代码。它将从环境变量加载继电器凭据,或者在使用 Defender 运行时使用注入的凭据。
const { Defender } = require('@openzeppelin/defender-sdk');
// 操作的入口点
exports.handler = async function(event) {
const client = new Defender(credentials);
// 使用 client.relaySigner 发送交易
}
// 在本地运行(此代码不会在操作中执行)
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
对象中发送你的操作期望的任何其他值,例如密钥或监视器事件。
你可以通过 Defender 界面编辑操作的代码,也可以使用 defender-sdk
npm 包以编程方式通过 API 进行编辑。后者允许上传包含多个文件的代码包:
代码包在压缩和 base64 编码后的大小不得超过 5MB,并且必须始终在 zip 文件的根目录中包含一个 index.js 作为入口点。 |
工作流允许你通过预定义的操作和场景立即检测、响应和解决威胁和攻击。你还可以在分叉的网络上进行攻击模拟并测试真实场景。
工作流是将自动操作和交易模板组合在一起的过程。操作可以并行运行或按顺序连接。工作流可以手动或通过 监控器(Monitor) 触发。
创建工作流是一种无缝的体验,通过一个表单引导你轻松地在工作流过程中组织操作。
要填充工作流,你必须将右侧列表中的现有操作拖到表单上。操作是垂直执行的,这意味着先前的操作必须成功完成才能开始执行新行。并行操作同时执行。但是,如果操作因错误而退出,则工作流将完全停止。
要并行运行多个操作,请单击“添加并行序列”并将操作拖到可用的并排框中。
你可以将操作从工作流中拖回以将其删除,或者单击右上角可见的减号图标以删除空步骤。右上角的“保存”按钮使用其配置和名称保存工作流。
我们提供了一个快速入门教程来创建和使用工作流。在此处查看 here! |
以下示例使用 ethers.js 和继电器集成来发送一个交易,调用给定合约上的 execute
。在发送交易之前,它会检查 canExecute
视图函数,并验证通过 webhook 收到的参数是否与本地密钥匹配。如果发送了交易,它会在响应中返回哈希,该哈希会发送回 webhook 调用者。
const { ethers } = require("ethers");
const { DefenderRelaySigner, DefenderRelayProvider } = require('defender-relay-client/lib/ethers');
// 操作的入口点
exports.handler = async function(event) {
// 加载 webhook 有效负载中提供的值(在计划或 sentinel 调用中不可用)
const { value } = event.request.body;
// 将其与本地密钥进行比较
if (value !== event.secrets.expectedValue) return;
// 初始化继电器提供程序和签名者
const provider = new DefenderRelayProvider(event);
const signer = new DefenderRelaySigner(event, provider, { speed: 'fast' });
// 从签名者创建合约实例并使用它发送交易
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 将负责监视交易并在需要时重新提交。操作只需要发送请求并退出。 |
每个操作的代码都在 Defender 中隔离,并且操作通过严格的访问控制受到限制,无法访问其他 Defender 内部基础设施。唯一的例外是操作可以访问其链接的继电器,这是通过操作服务在每次执行时注入的临时凭据协商的。尽管如此,操作只能调用继电器公开的方法,并且无法直接访问支持私钥或任何其他服务。
我们提供了一个快速入门教程,用于使用 Defender 为智能合约创建自动操作。在此处查看 here! |
- 原文链接: docs.openzeppelin.com/de...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!