Relayers

Relayers 允许您通过常规 API 请求或通过其他 Defender 模块(如 Actions、Workflows 和 Deploy)发送链上交易。Relayers 还会自动支付 Gas 费用,并处理私钥安全存储、交易签名、nonce 管理、Gas 价格估算和重新提交。使用 Relayers,您无需担心将私钥存储在后端服务器上,或者监控 Gas 价格和交易以确保它们得到确认。

用例

  • 自动执行智能合约上的交易以触发状态转换。

  • 使用外部数据更新链上 oracle。

  • 发送元交易以构建无 Gas 体验。

  • 通过将 token 空投给您的新用户来响应您应用中的注册。

  • 将资金从协议合约扫入安全钱包。

  • 构建具有完整自定义逻辑和灵活性的机器人。

什么是 Relayer?

Relayer 是一个基于 Ethereum 的外部所有账户 (EOA),专门分配给您的团队。每次您创建新的 Relayer 时,Defender 将在安全 vault 中创建一个新的私钥。每当您请求 Defender 通过该 Relayer 发送交易时,相应的私钥将用于签名。

您可以将每个 Relayer 视为发送交易的队列,其中通过同一 Relayer 发送的所有交易将按顺序从同一账户发送,该账户完全由您的团队控制。在此处 了解更多有关技术实现的信息

管理 Relayers

要创建 Relayer,只需单击页面右上角的 Create Relayer 按钮,指定一个名称并选择网络。

Manage Relayers Detail
请记住,您需要单独为每个 Relayer 充值 ETH(或原生链 token),以确保它们有足够的资金来支付您发送的交易的 Gas 费用。如果 Relayer 的资金低于 0.1 ETH,Defender 将向您发送电子邮件通知。
通过 Deploy 向导创建的测试网 Relayer 将在可能的情况下自动充值。在此处 阅读更多信息

API 密钥

每个 Relayer 可以关联一个或多个 API 密钥。为了通过 Relayer 发送交易,您需要使用 API 密钥/secret 对进行请求身份验证。您可以根据需要创建或删除 API 密钥,这不会更改发送地址或 Relayer 余额。

要为 Relayer 创建 API 密钥,请单击 Relayer,然后单击 More 按钮展开下拉菜单并选择 Create API Key

管理 Relayers 创建 API 密钥

创建 API 密钥后,请务必记下 secret 密钥。API secret 仅在创建期间可见一次 - 如果您不记下它,它将永远丢失。

管理 Relayer API 密钥
Relayer 的 API 密钥与其私钥无关。私钥始终保存在安全的密钥 vault 中,并且永远不会公开(有关更多信息,请参见 安全注意事项 部分)。这种解耦允许您在保持 Relayer 的相同地址的同时自由轮换 API 密钥。

地址

每当您创建 Relayer 时,将创建一个新的 EOA 来支持它。出于安全原因,无法将现有私钥导入到 Relayer 中,也无法导出 Defender 创建的 Relayer 的私钥。如果您授予 Relayer 地址在您的系统中的特权角色以避免锁定,请考虑使用管理方法将其切换到其他地址(如果需要)。

策略

您可以通过指定策略来限制 Relayer 的行为。

要配置 Relayer 的策略,请转到 Relayer 页面,选择 Relayer,然后转到 Policies 选项卡。然后,您将看到一个表单,您可以在其中选择启用策略并调整其参数。

Manage Relayer Policies

Gas 价格上限

指定使用 Relayer 发送的每笔交易的最大 Gas 价格。启用此策略后,Defender 将覆盖任何超出指定上限的交易的 gasPricemaxFeePerGas。请考虑到交易的 Gas 价格是根据 Relayer 实际发送交易以进行挖矿时 Gas 价格 oracle 指定的,因此此策略可以用作 Gas 价格飙升的保护。

除了您可以在此处指定的最大 Gas 价格策略之外,Defender 还为具有最低 Gas 要求的网络实施最低 Gas 价格策略。请与您使用的各个网络核实要求。

接收者白名单

指定使用 Relayer 发送的每笔交易的授权合约列表。Defender 将拒绝并丢弃任何目标地址不在列表中的交易。

白名单仅适用于交易的 to 字段。它不会过滤 ERC20 或其他资产接收者。

EIP1559 定价

指定 Relayer 发送的交易默认情况下是否应为 EIP1559。这适用于 Relayer 何时发送具有动态 Gas 价格或未指定的 gasPricemaxFeePerGas/maxPriorityFeePerGas 的交易。请注意,此策略选项仅针对与 EIP1559 兼容的网络显示。

默认情况下,新 Relayer 启用 EIP1559 定价策略。如果您有一个在没有默认选择加入的情况下创建的 Relayer,您可以随时启用此标志。

私人交易

指定是否应通过私人 mempool 发送交易。这意味着交易在包含在区块中之前不会公开可见。

该参数可以在以下状态之间切换:true 以启用私人 mempool 交易,false 以选择公开可见。或者,用户可以通过将值设置为 flashbots-normalflashbots-fast 来指定交易速度。默认情况下,当策略设置为 true 时,速度默认为 flashbots-normal,从而可以在保持交易隐私的同时实现无缝包含。此配置使户能够定制他们的交易策略,以有效地满足其特定的隐私和速度要求。您可以在此处 阅读有关使用 Flashbots 加快交易速度的信息

私人交易仅通过使用 Flashbots Protect RPC 为 _mainnet 启用。因此,相同的 关键注意事项 可能适用于通过 Defender 发送私人交易的情况。

Relayer 组

Relayer 组是协同工作以提交交易的单个 relayer 的集合。通过对 relayer 进行分组,您可以提高整体交易吞吐量和冗余,从而提高交易提交过程的可靠性。Relayer 组旨在将工作负载分配到多个 relayer,确保没有单个 relayer 成为瓶颈。

Relayer 组的优势

  • 提高吞吐量: Relayer 组可以处理更大的交易量,因为工作负载分布在多个 relayer 上。

  • 冗余: 如果组中的一个 relayer 发生故障或速度变慢,其他 relayer 可以接管,从而降低延迟的风险。

  • 效率: 通过协调多个 relayer,您可以优化交易提交并确保尽快处理交易。

  • 集中管理: 在单个 API 密钥下管理多个 relayer 的能力简化了管理,从而更容易维护对复杂系统的控制。

Relayer 组的缺点

  • 统一配置: 策略和配置统一应用于组中的所有 relayer,从而难以管理单个 relayer 设置。

  • 功能有限: 某些功能(如消息签名)对于作为组一部分的 relayer 不可用。

  • 组限制操作: 组中的 Relayer 不能独立使用;它们必须作为组的一部分协同工作。

  • 潜在的交易顺序问题: 由于交易根据其条件分布在不同的 relayer 之间,因此它们可能不会按照接收到的顺序进行处理,从而导致某些交易按错误顺序进行 Mining。

健康监控

Relayer 组依赖于定期健康检查,以确保交易在组中的 relayer 之间高效分配。这些健康检查评估每个 relayer 的性能和可用性,帮助系统确定哪些 relayer 最适合处理新交易。

系统定期评估组中的每个 relayer。它根据几个关键因素计算每个 relayer 的“权重”。然后,这些权重用于确定如何在组内分配交易,并优先考虑最可靠和响应速度最快的 relayer。

  • 首次交易处理速度(最高优先级): 最重要的因素是 relayer 开始处理交易的速度。系统会查看发送和处理第一笔待处理交易所需的时间。速度更快的 relayer 被认为更健康,并被赋予更高的优先级。

  • 待处理交易数量: 系统会检查每个 relayer 的队列中有多少交易在等待。如果一个 relayer 有大量待处理交易,则可能表明它已超载,并且可能难以快速处理新交易。

  • 剩余余额: 还会考虑 relayer 的可用余额。Relayer 需要足够的余额来支付交易费用。如果 relayer 的余额较低,则可能难以处理交易,这会影响其健康评分。

用户可以手动调整单个 relayer 的权重。例如,将权重设置为 0 将阻止使用 relayer,从而可以精确控制哪些 relayer 处于活动状态。

发送交易

通过 Relayer 发送交易的最简单方法是使用 Defender SDK 包。客户端使用 API 密钥/secret 进行初始化,并公开一个简单的 API,用于通过相应的 Relayer 发送交易。

const { Defender } = require('@openzeppelin/defender-sdk');
const client = new Defender({
  relayerApiKey: 'YOUR_API_KEY',
  relayerApiSecret: 'YOUR_API_SECRET'
});

const tx = await client.relayerSigner.sendTransaction({
  to, value, data, gasLimit, speed: 'fast'
});

const mined = await tx.wait();

为了提高 relayer 的可靠性,我们建议在单个 relayer 上发送的交易量不超过 50 笔/分钟,尤其是在 Polygon、Optimism、Arbitrum 等快速移动的链上。例如,如果您想要 250 笔交易/分钟的吞吐量,则需要在 5 个 Relayer 之间进行负载均衡。这 5 个 Relayer 可以是同一账户的一部分。

初始化 Relayer 客户端时,无需输入私钥,因为私钥安全地保存在 Defender vault 中。
目前,zkSync 除了使用 eth_estimateGas 端点外,没有其他方法可以精确计算 gasLimit。因此,Defender 无法执行任何 gasLimit,并将用户输入覆盖为 RPC 估算值。

使用 ethers.js

Relayer 客户端通过自定义 signerethers.js 集成。这允许您切换到 Relayer 并发送交易,而无需对您的代码库进行最小的更改。

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

const credentials = { relayerApiKey: YOUR_RELAYER_API_KEY, relayerApiSecret: YOUR_RELAYER_API_SECRET };
const client = new Defender(credentials);

const provider = client.relaySigner.getProvider();
const signer = client.relaySigner.getSigner(provider, { speed: 'fast', validUntil });

const erc20 = new ethers.Contract(ERC20_ADDRESS, ERC20_ABI, signer);
const tx = await erc20.transfer(beneficiary, 1e18.toString());
const mined = await tx.wait();

在上面的示例中,我们还使用 DefenderRelayProvider 来调用网络。signer 可以与任何 provider 一起使用,例如 ethers.getDefaultProvider(),但您也可以依赖 Defender 作为网络 provider。

您可以在 此处 阅读有关 ethers 集成的更多信息。

使用 web3.js

Relayer 客户端还通过自定义 providerweb3.js 集成。这允许您使用 Relayer 发送交易,并使用熟悉的 web3 界面查询网络。

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

const credentials = { relayerApiKey: YOUR_RELAYER_API_KEY, relayerApiSecret: YOUR_RELAYER_API_SECRET };
const client = new Defender(credentials);

const provider = client.relaySigner.getProvider();

const web3 = new Web3(provider);

const [from] = await web3.eth.getAccounts();
const erc20 = new web3.eth.Contract(ERC20_ABI, ERC20_ADDRESS, { from });
const tx = await erc20.methods.transfer(beneficiary, (1e18).toString()).send();

在上面的示例中,transfer 交易由 Relayer 签名和广播,任何其他 JSON RPC 调用都通过 Defender 私有端点路由。

您可以在 此处 阅读有关 web3 集成的更多信息。

使用 Relay Signer 的 Intents 支持

当使用 Defender SDK 的 relay signer 从 ethers/web3 合约的上下文中发送交易时,重要的是要注意不支持 intent 模式 中的交易。如果 API 返回 intent 响应,则会抛出错误,因为当前实现期望某些在 intent 响应中不存在的字段。

为了避免此问题,建议回退到使用默认 SDK sendTransaction 方法进行交易提交。这确保了交易可以无错误地处理。

此外,当前的实现不跟踪重新提交的交易,这可能会导致由于重试而生成多个哈希时出现问题。实施可以有效处理这些场景的逻辑至关重要,以确保即使在后台更改交易哈希的情况下,该过程也能完成。

EIP1559 支持

由于并非所有受支持的网络都与 EIP1559 兼容,因此 EIP1559 交易支持仅适用于那些被标识为兼容且由团队启用的网络

Relayer 可以通过以下方式发送 EIP1559 交易:

  • 通过 UI 发送交易,并 EIP1559Pricing 策略已启用

  • 通过 API 发送交易,并指定 maxFeePerGasmaxPriorityFeePerGas

  • 通过 API 发送交易,并使用 speed,且 EIP1559Pricing 策略已启用

发送任何交易后,它将在其生命周期的每个阶段都具有相同的类型(例如替换和重新定价),因此目前无法更改类型(如果已提交)。

任何尝试将 maxFeePerGasmaxPriorityFeePerGas 发送到非 EIP1559 兼容网络的尝试都将被 Relayer 拒绝和丢弃。

您可以通过查看 Relayer 的 策略 来判断网络是否支持 EIP1559。如果 EIP1559Pricing 策略未显示,则表示我们尚未为该网络添加 EIP1559 支持。

如果您发现我们已经支持的 EIP1559 兼容网络但未启用 EIP,请随时通过 https://www.openzeppelin.com/defender2-feedback 与我们联系。

私人交易

私人交易允许 Relayer 发送交易,而无需在公共 mempool 上可见,而是通过使用特殊的 eth_sendRawTransaction provider 的私人 mempool 转发交易,这将根据网络和当前支持而有所不同(例如 Flashbots 网络覆盖)。

Relayer 可以通过以下任何方式发送私人交易:

  • 通过 API 发送交易,并 privateTransactions 策略已启用或设置为 flashbots-normalflashbots-fast

  • 通过 API 发送交易,并将 isPrivate 参数设置为 true

  • 通过 UI 发送交易,并选中 Mempool Visibility 复选框

Relayer 的发送交易视图上的 Mempool 可见性复选框
isPrivate 标志设置为 true 的交易发送到不支持私人交易的网络将被 Relayer 拒绝和丢弃。

目前,仅支持以下网络

速度

Relayer 还可以接受 speed 参数,而不是通常的 gasPricemaxFeePerGas/maxPriorityFeePerGas,该参数可以是 safeLowaveragefastfastest。当发送或重新提交交易时,这些值会映射到实际 Gas 价格,并且会因网络状态而异。

如果提供了速度,则将根据 EIP1559Pricing Relayer 策略对交易进行定价。

Mainnet Gas 价格和优先级费用是根据 EthGasStation、https://etherchain.org/tools/gasPriceOracle[EtherChain, window=_blank]、https://www.gasnow.org/[GasNow, window=_blank]、https://docs.blocknative.com/gas-platform[Blockative, window=_blank] 和 Etherscan 报告的值计算得出的。在 Polygon 及其测试网中,使用 Gas 站。在其他网络中,Gas 价格是从对网络的 eth_gasPriceeth_feeHistory 调用中获得的。

固定 Gas 定价

或者,您可以通过设置 gasPrice 参数或 maxFeePerGasmaxPriorityFeePerGas 参数,为交易指定固定 GasPricemaxFeePerGas 和 maxPriorityFeePerGas 的固定组合。具有固定定价的交易要么使用指定的定价进行 Mining,要么在 validUntil 时间之前无法进行 Mining 时,将替换为 NOOP 交易。

请记住,您必须提供 speedgasPricemaxFeePerGas/maxPriorityFeePerGas 或不提供任何参数,但不要在发送交易请求中混合使用它们。

每当在没有定价参数的情况下发送发送交易请求时,将使用 fast 默认速度对其进行定价。
如果您同时提供固定的 maxFeePerGasmaxPriorityFeePerGas,请确保 maxFeePerGas 大于或等于 maxPriorityFeePerGas。否则,它将被拒绝。

有效期至

通过 Relayer 发送的每笔交易在 validUntil 时间之前对提交给网络有效。在 validUntil 时间之后,该交易将替换为 NOOP 交易,以防止 Relayer 卡在交易的 nonce 上。NOOP 交易除了推进 Relayer 的 nonce 之外,什么也不做。

validUntil 默认为交易创建后 8 小时。请注意,您可以将 validUntil 与 固定定价 结合使用,以实现极快的 Mining 时间并在 gasPricemaxFeePerGas 上击败其他交易。

如果您使用 ethers.js,则可以设置 validForSeconds 选项而不是 validUntil。在下面的示例中,我们配置了一个 DefenderRelaySigner 来发出一个在创建后 120 秒内有效的交易。

const { DefenderRelayProvider, DefenderRelaySigner } = require('@openzeppelin/defender-sdk-relay-signer-client/ethers');
const { ethers } = require('ethers');

const credentials = { apiKey: API_KEY, apiSecret: API_SECRET };
const provider = new DefenderRelayProvider(credentials);
const signer = new DefenderRelaySigner(credentials, provider, { speed: 'fast', validForSeconds: 120 });
validUntil 是一个 UTC 时间戳。请确保使用 UTC 时区而不是本地时区。

交易 ID

由于 Relayer 可能会在给定的交易未在预期的时间范围内确认的情况下,以更新的 Gas 定价重新提交交易,因此给定交易的 hash 可能会随着时间的推移而发生变化。要跟踪给定交易的状态,Relayer API 会返回一个 transactionId 标识符,您可以使用该标识符来 查询 它。

import { Relayer } from '@openzeppelin/defender-sdk-relay-signer-client';
const relayer = new Relayer({ apiKey: API_KEY, apiSecret: API_SECRET });
const latestTx = await relayer.getTransaction(tx.transactionId);

返回的交易对象 latestTx 将具有以下形状:

interface RelayerTransactionBase {
  transactionId: string; // Defender 交易标识符
  hash: string; // Ethereum 交易哈希
  to: string;
  from: string;
  value?: string;
  data?: string;
  speed: 'safeLow' | 'average' | 'fast' | 'fastest';
  gasLimit: number;
  nonce: number;
  status: 'pending' | 'sent' | 'submitted' | 'inmempool' | 'mined' | 'confirmed' | 'failed';
  chainId: number;
  validUntil: string;
}
getTransaction 函数将从 Defender 服务返回交易的最新视图,该视图每分钟更新一次。

替换交易

虽然 Relayer 会自动重新提交 Gas 定价增加的交易(如果它们未被确认),并且会在其有效期至时间戳之后自动取消它们,但如果您的交易尚未被 Mining,您仍然可以手动替换或取消您的交易。这允许您在交易不再有效时取消交易,调整其 TTL,或提高其速度或 Gas 定价。

为此,请使用 @openzeppelin/defender-sdk-relay-clientreplaceByNoncereplaceById

// Cancel tx payload (tx to a random address with zero value and data)
replacement = {
  to: '0x6b175474e89094c44da98b954eedeac495271d0f',
  value: '0x00',
  data: '0x',
  speed: 'fastest',
  gasLimit: 21000
};

// Replace a tx by nonce
tx = await relayer.replaceTransactionByNonce(42, replacement);

// Or by transactionId
tx = await relayer.replaceTransactionById('5fcb8a6d-8d3e-403a-b33d-ade27ce0f85a', replacement);

您还可以在使用 ethersweb3.js 适配器发送交易时通过设置 nonce 来替换待处理的交易:

// Using ethers
erc20 = new ethers.Contract(ERC20_ADDRESS, ERC20_ABI, signer);
replaced = await erc20.functions.transfer(beneficiary, 1e18.toString(), {
  nonce: 42
});

// Using web3.js
erc20 = new web3.eth.Contract(ERC20_ABI, ERC20_ADDRESS, { from });
replaced = await erc20.methods.transfer(beneficiary, (1e18).toString()).send({
  nonce: 42
});
只能替换相同类型的交易。例如,如果您尝试替换 EIP1559 交易,则不能将其替换为旧式交易。此外,如果改为提供 speed,该交易将以其要求的原始类型以给定的速度重新定价。

Webhooks 通知

使用基于推送的方法(通过 webhooks)可以有效地实现侦听交易状态更改。此方法允许您的应用程序在交易状态有更新时接收实时通知。

配置 Webhook 通知的步骤

  1. 访问 Relayers 页面

  2. 选择交易状态: 在 Relayers 页面上,您会找到用于选择要接收通知的特定交易状态的选项。可用状态:

    • Pending:Defender 接收到的交易。

    • Sent:准备好发送的交易(已定价和签名)。

    • Submitted:已提交到网络的交易。

    • InMemPool:在 mempool 中找到的交易。

    • Mined:已挖掘的交易。

    • Confirmed:已确认的交易(至少 12 次确认)。

  3. 选择通知渠道: 选择所需状态后,您需要选择 webhook 通知渠道。这是发送 webhook 通知的位置。

  4. 保存您的配置: 选择状态并配置通知渠道后,保存您的设置。这将注册您的 webhook 并启动接收所选事件的通知的过程。

Webhook 通知的示例:

{
  "event": "transaction_status_change",
  "timestamp": "2024-06-13T12:29:41.254Z",
  "transaction": {
    "signature": {
      "r": "0xee81d58c53c1d3432c95847c71a525417bad6e8fa711007137b2e11155ba8f94",
      "s": "0x362ce1f75d660504e22590f678c243bc24954ccfbf17864f4aab05fd8b1d6ca3",
      "v": "0x1b"
    },
    "maxPriorityFeePerGas": 7556907973,
    "maxFeePerGas": 39004490874,
    "chainId": 11155111,
    "hash": "0xea04c34422295ef60b57fea50790b4f9396d852274fa46ee5cf8d0407d7cc32b",
    "transactionId": "1eece8bb-05d6-493f-903a-12c750700b81",
    "value": "0x5af3107a4000",
    "gasLimit": 25200,
    "to": "0x5e87fD270D40C47266B7E3c822f4a9d21043012D",
    "from": "0xf87921a0999d522383afa2b41db2538231a647f0",
    "data": "0x",
    "nonce": 19,
    "status": "mined",
    "speed": "fast",
    "validUntil": "2024-06-13T20:29:01.051Z",
    "createdAt": "2024-06-13T12:29:01.546Z",
    "sentAt": "2024-06-13T12:29:01.546Z",
    "pricedAt": "2024-06-13T12:29:01.546Z",
    "isPrivate": false
  }
}

列出交易

您还可以列出通过您的 Relayer 发送的最新交易,可以选择按状态(待处理、已 Mining 或失败)进行过滤。这对于防止您的 Actions 脚本重新发送已在进行中的交易尤其有用:在发送交易之前,您可以使用按 pending 状态过滤的列表方法来查看队列中是否有一个与您即将发送的交易具有相同目标和 calldata 的交易。

const txs = await relayer.list({
  since: new Date(Date.now() - 60 * 1000),
  status: 'pending', // 可以是 'pending'、'mined' 或 'failed'
  limit: 5, // 最新的交易将首先返回
  usePagination: true,
  next: '' // 可选的下一个光标用于分页
  sort: 'desc'
})

删除待处理交易

在 relayer 卡住且无法处理交易的情况下,系统提供删除待处理交易的功能。此操作旨在解决至少 30 分钟未被 Mining 的交易的问题。当 relayer 有待处理交易时,可以从 Relayer 抽屉的“待处理交易”选项卡中激活此功能。

关键点:

  • 预期用途:此功能专门用于解决因未 Mining 的交易而卡住的 relayer 的问题。建议仅在确认至少 30 分钟内没有 Mining 任何交易后才使用此功能。

  • 操作概述:启动删除操作后,relayer 将进入暂停状态。在此暂停期间,系统将发送 NOOP(无操作指令)以清除某些交易,或将它们完全从数据库中删除。此区别是根据每个待处理交易的特定特征做出的。

  • 持续时间:删除待处理交易和恢复正常操作的整个过程可能需要长达 30 分钟。这包括评估每个交易、应用必要操作以及确保 relayer 准备好恢复其功能所花费的时间。

  • 恢复操作:删除操作完成后,relayer 将自动恢复其标准活动。用户无需采取任何进一步的操作来重新激活 relayer。

  • 通知:一旦该过程完成并且 relayer 恢复其操作,用户将收到一封电子邮件通知。

Intent 机制

“intent”是一个引入的概念,旨在提高交易提交的效率和可靠性。intent 不是立即提交交易,而是一个占位符,指示交易已准备好发送,但暂时被搁置。此机制有助于管理交易队列变得太大或网络或 relayer 处理交易速度缓慢的情况。

intent 用于维护交易的顺序并防止系统过载。在两种主要情况下,intent 会发挥作用:

  • 高交易量: 当待处理交易的数量超过允许的最大飞行中交易数时,新交易将存储为 intent。这可以防止系统不堪重负,并确保以正确的顺序提交交易。

  • 处理缓慢: 如果网络或 relayer 处理交易速度缓慢(例如,在过去 30 分钟内没有 Mining 任何交易),则新交易将存储为 intent,以避免增加拥塞。

Intent 响应

Intent 响应类似于正常交易的响应,但有以下区别:

  • hashnull

  • isIntenttrue 并指示交易作为 intent 发送,这不会为正常交易设置。当 intent 被处理时,此字段不会被删除或更改。

  • pendingIntentSince 是创建 intent 的时间戳,并指示 intent 仍在待处理中。一旦 intent 被处理,此字段将被删除。

  • relayerId 与创建时 relayer 组的 ID 相同。一旦分配完成,这将更改为将处理 intent 的 relayer 的 ID。

  • 不设置与 Gas 相关的字段,交易将在处理时进行定价。

{
  "hash": null,
  "transactionId": "transaction123",
  "value": "0x1",
  "gasLimit": 21000,
  "to": "0x123...",
  "data": "0x",
  "status": "pending",
  "speed": "fast",
  "validUntil": "2000-01-01T20:00:00.000Z"
  "isIntent": true,
  "relayerId": "relayer123",
  "tenantRelayerGroupId": "tenant123|relayerGroup123",
  "createdAt": "2000-01-01T12:00:00.000Z"
}

取消 Intents

Relayer 交易 intents 可以被取消并从我们的后端永久删除。当您需要阻止提交特定 intent 或想要释放队列中的空间以用于其他更紧急的交易时,此功能特别有用。

为此,请使用 @openzeppelin/defender-sdk-relay-clientcancelTransactionById 方法:

tx = await relayer.cancelTransactionById('5fcb8a6d-8d3e-403a-b33d-ade27ce0f85a');

签名

除了发送交易之外,Relayer 还可以使用其私钥根据 EIP-191 标准(前缀为 \x19Ethereum Signed Message:\n)对任意消息进行签名。您可以通过客户端的 sign 方法或等效的 ethers.js 方法访问此功能。

const signResponse = await relayer.sign({ message });
与大多数库相反,Relayers关于 Relayer 的更多信息,请查看客户端的 getRelayer 方法。它返回以下数据:
const info = await relayer.getRelayer();
console.log('Relayer info', info);

export interface RelayerModel {
  relayerId: string;
  name: string;
  address: string;
  network: string;
  paused: boolean;
  createdAt: string;
  pendingTxCost: string;
}
对于属于 relayer 组的 relayer,此方法将返回 relayer 组信息。

Relayer 状态

为了更好地了解 relayer 的当前状态,可以使用 DefenderRelaySigner 类的 getRelayerStatus 方法。此方法提供关于 relayer 的实时信息,例如其 nonce、交易配额和待处理交易的数量。

const address = await signer.getRelayerStatus();

如果您需要关于 Relayer 的信息,请查看客户端的 getRelayer 方法。它返回以下数据:

export interface RelayerStatus {
  relayerId: string;
  name: string;
  nonce: number;
  address: string;
  numberOfPendingTransactions: number;
  paused: boolean;
  pendingTxCost?: string;
  txsQuotaUsage: number;
  rpcQuotaUsage: number;
  lastConfirmedTransaction?: {
    hash: string;
    status: string;
    minedAt: string;
    sentAt: string;
    nonce: number;
  };
}
对于属于 relayer 组的 relayer,此方法将返回组的状态响应数组。

网络调用

Defender 还提供了一种简便的方法来对网络进行任意 JSON RPC 调用。您可以使用底层 relayer.call 方法来发送任何 JSON RPC HTTP 请求:

const balance = await relayer.call('eth_getBalance', ['0x6b175474e89094c44da98b954eedeac495271d0f', 'latest']);

如果您正在使用 ethers.js,则可以通过自定义的 DefenderRelayProvider provider 对象来支持:

const provider = new DefenderRelayProvider(credentials);
const balance = await provider.getBalance('0x6b175474e89094c44da98b954eedeac495271d0f');

提取资金

您可以在 Relayers 页面上从 Relayer 提取资金,选择 Relayer,然后单击 Withdraw

Relayer 提款按钮

Withdraw 屏幕上,您可以选择以 ETH 发送资金,或者从内置的 ERC20 代币列表中选择。

Relayer 提款资金屏幕

底层原理

每个 Relayer 都与一个私钥相关联。当收到发送交易的请求时,Relayer 会验证该请求,原子地为其分配一个 nonce,保留余额以支付其 gas 费用,根据其 EIP1559 定价策略将其速度解析为 gasPricemaxFeePerGas / maxPriorityFeePerGas,使用其私钥对其进行签名,并将其排队以提交到区块链。仅在此过程完成后,响应才会发送回客户端。然后,交易通过多个节点提供商进行广播以实现冗余,并在 API 出现故障时最多重试三次。

每分钟,系统都会检查所有正在进行的交易。如果它们尚未被挖掘,并且超过了特定时间(这取决于交易速度),则会以各自交易类型定价增加 10%(或其速度的最新定价,如果更高)重新提交,这可能高达*其速度的报告 gas 定价的 150%*。此过程会导致交易哈希更改,但其 ID 会保留。另一方面,如果交易已被挖掘,它仍将被监视几个区块,直到我们认为它已被确认。

资金不足

Defender 仔细跟踪发送到 relayer 的交易成本。在 relayer 无法立即处理交易的情况下,Defender 会累积所有待处理交易的成本。在允许提交任何新交易之前,Defender 将确保 relayer 的余额足以支付包括新交易在内的所有待处理交易的成本。因此,在此类情况下,您可能会遇到特定交易的“资金不足”错误。

  • 交易成本计算为:txCost = gasLimit * maxFeePerGas + value

  • relayer 的余额计算为:predictedBalance = balance - pendingTxCosts

txCost > predictedBalance 时,将抛出“资金不足”错误。

交易吞吐量和负载均衡

我们建议使用 relayer 组来增加吞吐量和冗余。但是,如果这不是一个选项,您可以使用以下方法来优化您的设置。

Relayer 以原子方式分配 nonce,这允许它们处理许多并发交易。但是,确实存在优化基础设施的限制(以下所有数字都是帐户中所有 Relayer 的累积值)

默认情况下,当您为特定的 relayer 创建 API 密钥时,它会自动分配以下速率限制。

  • 每秒 100 个请求,突发 300 个请求。

这些速率限制适用于读取(例如 - 获取交易状态)和写入(例如 - 发送交易)。

如果您的用例需要额外的吞吐量,请联系 defender-support@openzeppelin.com。您需要处于 enterprise tier 才能提高吞吐量。

为了提高 relayer 的可靠性,我们建议在单个 relayer 上每分钟发送不超过 50 笔交易,尤其是在 Polygon、Optimism、Arbitrum 等快速移动的链上。例如,如果您想要每分钟 250 笔交易的吞吐量,您需要在 5 个 relayer 上进行负载均衡。这 5 个 relayer 可以是同一帐户的一部分。

您可以使用 Defender SDK 包在多个 relayer 上进行负载均衡。以下是一个简单的示例,说明如何执行此操作:

require('dotenv').config();

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

async function loadbalance() {
  const LOAD_BALANCE_THRESHOLD = 50;
  const relayerCredsForMainNet = [
    {
      relayerApiKey: process.env.RELAYER_API_KEY_1,
      relayerApiSecret: process.env.RELAYER_API_SECRET_1,
    },
    {
      relayerApiKey: process.env.RELAYER_API_KEY_2,
      relayerApiSecret: process.env.RELAYER_API_SECRET_2,
    },
  ];
  const relayerClientsForMainNet = relayerCredsForMainNet.map((creds) => new Defender(creds));

  const getNextAvailableRelayer = async () => {
    for (const client of relayerClientsForMainNet) {
      const relayerStatus = await client.relaySigner.getRelayerStatus();
      if (relayerStatus.numberOfPendingTransactions < LOAD_BALANCE_THRESHOLD) {
        return client;
      }
      console.log(
        `${relayerStatus.relayerId} is busy. Pending transactions: ${relayerStatus.numberOfPendingTransactions}/${LOAD_BALANCE_THRESHOLD}`,
      );
    }
    return undefined;
  };

  const executeTransaction = async () => {
    const client = await getNextAvailableRelayer();
    if (!client) throw new Error('Unable to load balance. All relayers are operating above the suggested threshold.');

    const txResponse = await client.relaySigner.sendTransaction({
      to: '0x179810822f56b0e79469189741a3fa5f2f9a7631',
      value: 1,
      speed: 'fast',
      gasLimit: '21000',
    });
    console.log('txResponse', JSON.stringify(txResponse, null, 2));
  };

  await executeTransaction();
}

async function main() {
  try {
    return await loadbalance();
  } catch (e) {
    console.log(`Unexpected error:`, e);
    process.exit(1);
  }
}

if (require.main === module) {
  main().catch(console.error);
}

安全考虑

所有私钥都存储在 AWS Key Management Service 中。密钥在 KMS 中生成,并且永远不会离开它,即所有签名操作都在 KMS 中执行。此外,我们依靠动态生成的 AWS Identity and Access Management 策略来隔离租户之间对私钥的访问。

至于 API 密钥,它们仅在创建时发送到客户端时保存在内存中。之后,它们会被哈希处理并安全地存储在 AWS Cognito 中,AWS Cognito 在后台用于验证 Relayer 请求。这使得 API 密钥易于轮换,同时保留 KMS 上的相同私钥。

Rollups

当将交易发送到 rollup 链(例如 Arbitrum 或 Optimism)时,Relayer 当前依赖于链的 sequencer/aggregator。这意味着,如果 sequencer 出现故障或审查交易,Relayer 将不会绕过它并直接提交到 layer 1。

不活跃状态

如果 Testnet relayer 在超过 60 天内没有发送任何交易,则会被认为是不活跃的。当 testnet relayer 处于非活动状态时,我们会提供 14 天的宽限期来将 relayer 标记为活动状态。如果用户不采取任何措施,relayer 将在此期限结束后自动删除。