本文介绍了如何使用 Gelato Relay 为智能合约启用 gasless 交易,从而构建为用户提供 gasless 体验的应用程序。通过一个 DAO 提案投票的示例应用,展示了如何将 createProposal() 和 vote() 交易转换为 gasless 交易,并使用 Gelato Automate 自动关闭投票提案。
摘要:
我们将从一个允许 DAO 成员发送提案的应用开始。一旦提案被发送,用户有 30 分钟的时间进行投票,之后一个自动化的 Gelato 任务将结束投票期。
最初,每个提案和投票都是签名交易。github 仓库可以在这里找到。
要开始,打开你的命令行并输入以下内容:
git clone https://github.com/donoso-eth/gasless-voting
cd gasless-voting
git checkout main
yarn
在第二个终端中,确保运行一个本地的 hardhat 节点:
1 终端:
npm run fork
2 终端:
npm run compile
npm run deploy
我们的前端是用 angular 构建的,我们用以下命令启动它:
npx ng serve -o
这将在 http://localhost:4200/ 打开一个浏览器选项卡。在本地 hardhat 节点中,我们可以通过创建提案和投票来进行测试。稍后,我们将部署到测试网来测试中继的交易,因为这涉及到链下基础设施。
许多 DAO 都在努力解决的一个问题是成员参与度,尤其是当成员由于 gas 限制而难以对提案进行投票时。为了消除投票支付 gas 的摩擦,我们将把我们的合约转换成一个 relay-aware 的合约。但首先,让我们深入了解一下 relayer 的工作原理。
要实现 relayer,我们需要决定如何资助交易,以及是否需要 Gelato 的支持来验证用户身份。
以下是我们简化流程的方法:DAO 可以轻松创建一个应用,允许用户创建提案和投票,而无需支付 gas。
我们将把 createProposal()
和 vote()
交易转换为 gasless 交易。
按照下表,我们将确定要使用哪个合约和 SDK 方法。
Gelato 认证 | 付款方式 | 继承合约 | SDK/API 方法 |
---|---|---|---|
否 | 用户 | GelatoRelayContext | relayWithSyncFee |
是 | 用户 | GelatoRelayContextERC2771 | relayWithSyncFeeERC2771 |
否 | 1Balance | n. a. | relayWithSponsoredCall |
是¹ | 1Balance | ERC2771Context | relayWithSponsoredCallERC2771 |
在这种情况下,我们允许所有用户创建提案,因此无需验证用户身份。通常我们建议使用 1Balance 作为付款方式,但对于本演示,我们将使用 Gelato Relay 的 SyncFee
付款方式。
如果我们按照上表,我们位于第一行,不需要验证用户身份。
我们当前的 solidity 交易看起来像这样:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract GaslessProposing {
// @notice createProposal Transaction
// @dev external
function createProposalTransaction(bytes calldata payload) external {
require(
proposal.proposalStatus == ProposalStatus.Ready,
"OLD_PROPOSAL_STILL_ACTIVE"
);
proposalId++;
proposal.proposalStatus = ProposalStatus.Voting;
proposal.proposalId = proposalId;
proposalTimestamp = block.timestamp;
proposalBytes = payload;
IGaslessVoting(gaslessVoting)._createProposal(proposalId, payload);
finishingVotingTask = createFinishVotingTask();
proposal.taskId = finishingVotingTask;
emit ProposalCreated(finishingVotingTask);
}
}
我们将按照以下步骤将我们的合约转换为 relay-aware 合约:
安装 Relay-Context 合约
npm i @gelatonetwork/relay-context
导入 relay-context 合约:
import {GelatoRelayContext} from "@gelatonetwork/relay-context/contracts/GelatoRelayContext.sol";
继承 relay-context 合约:
contract GaslessProposing is GelatoRelayContext {
最后,我们将使用 onlyGelatoRelay()
修饰符限制我们的 createProposal
方法,并使用辅助函数 _transferRelayFee()
将费用转移到 Gelato Relay。
如果没有向正确的 feeCollector
地址 支付正确的费用,Gelato 将不会执行你的交易
// @notice
// @dev external only Gelato relayer
// @dev transfer Fee to Geato with _transferRelayFee();
function createProposal(bytes calldata payload) external onlyGelatoRelay {
require(
proposal.proposalStatus == ProposalStatus.Ready,
"OLD_PROPOSAL_STILL_ACTIVE"
);
_transferRelayFee();
proposalId++;
proposal.proposalStatus = ProposalStatus.Voting;
proposal.proposalId = proposalId;
proposalTimestamp = block.timestamp;
proposalBytes = payload;
IGaslessVoting(gaslessVoting)._createProposal(proposalId, payload);
finishingVotingTask = createFinishVotingTask();
proposal.taskId = finishingVotingTask;
emit ProposalCreated(finishingVotingTask);
}
通过这些更改,我们的合约现在是 relay-aware 的,并准备好接收 gasless 交易。如果我们深入了解一下,我们可以看到应用的更改:
createProposal()
方法calldata
的费用、费用收集器和 feeToken,并将其用于将费用转移到 Gelato现在我们的合约已准备就绪,我们必须更新我们的前端调用合约的方式。
在这里,我们可以看到我们到目前为止如何调用交易:
async createProposal() {
let name = this.proposalForm.controls.nameCtrl.value;
let description = this.proposalForm.controls.descriptionCtrl.value;
let payload = this.abiCoder.encode(
['string', 'string'],
[name, description]
);
await doSignerTransaction(
this.gaslessProposing.createProposalTransaction(payload)
);
}
现在让我们把我们的交易改为 relay-SDK 调用 Gelato Relay。
首先,我们将安装 Gelato Relay SDK,构建并发送请求
npm i @gelato-network/relay-sdk:
导入 SDK 和相关方法
import { CallWithSyncFeeRequest, GelatoRelay } from '@gelatonetwork/relay-sdk';
实例化 GelatoRelay 对象
const relay = new GelatoRelay();
按照 文档 构建请求
const request = {
chainId // 网络
target // 目标合约地址
data // 编码的交易数据
isRelayContext // 我们是否使用 context 合约
feeToken // 用于支付 relayer 的 token
};
我们使用原生 token,我们在 Goerli 上,我们知道目标合约地址,所以唯一缺少的部分是数据(编码的交易数据)
const { data } =
await this.readGaslessProposing.populateTransaction.createProposal(payload);
最后,我们使用 SDK 方法 callWithSyncFee
发送我们的请求并检索任务 ID。
// 将 relayRequest 发送到 Gelato Relay API
const relayResponse = await relay.callWithSyncFee(request);
let taskId = relayResponse.taskId
我们最终的代码如下所示:
async createProposal() {
let name = this.proposalForm.controls.nameCtrl.value;
let description = this.proposalForm.controls.descriptionCtrl.value;
let payload = this.abiCoder.encode(
['string', 'string'],
[name, description]
);
const feeToken = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
const { data } =
await this.readGaslessProposing.populateTransaction.createProposal(
payload
);
// 填充 relay SDK 请求 body
const request = {
chainId: 5, // Goerli 在这种情况下
target: this.readGaslessProposing.address, // 目标合约地址
data: data!, // 编码的交易数据
isRelayContext: true, // 我们是否使用 context 合约
feeToken: feeToken, // 用于支付 relayer 的 token
};
// 将 relayRequest 发送到 Gelato Relay API
const relayResponse = await relay.callWithSyncFee(request);
console.log(relayResponse);
let taskId = relayResponse.taskId
}
瞧!我们的第一个 gasless 交易就完成了!在此处查看演示应用 https://gelato-gasless-dao.web.app/landing。
现在假设我们的应用希望每个用户只有一个投票。在这种情况下,我们将需要验证用户身份,并且我们将使用 1Balance 来赞助我们的交易。
如果我们再次按照方便的表格,此用例与最后一行匹配:
我们将在 此处 配置 1Balance beta,你可以在 此处 找到有关如何执行此操作的更多信息。
我们存入 GETH:
我们点击 Relay Apps 选项卡,点击“创建应用”并输入目标合约地址和要调用的方法。
最后,我们将从 API Key 选项卡复制 API 密钥 (sponsorApikey),以便稍后在我们需要发送请求时使用。
我们当前的 Solidity 交易如下所示:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
contract GaslessVoting {
// @notice voting proposal
// @dev
function votingProposal(bool positive) external {
address voter = msg.sender;
_votingProposal(positive, voter);
emit ProposalVoted();
}
}
我们将按照以下步骤将我们的合约转换为 relay-aware 合约:
安装 Relay-Context 合约(我们在上面的示例中已经完成了)
npm i @gelatonetwork/relay-context
导入 relay-context 合约:
import {
ERC2771Context
} from "@gelatonetwork/relay-context/contracts/vendor/ERC2771Context.sol";
继承 relay-context 合约,我们传递可信转发器的地址 0xBf1..
contract GaslessVoting is ERC2771Context {
constructor() ERC2771Context(address(0xBf175FCC7086b4f9bd59d5EAE8eA67b8f940DE0d)) { }
最后,我们更新了我们的方法,包括 onlyTrustedForwarder
方法
// @notice voting proposal
// @dev function called by the relaer implementing the onlyTrusted Forwarder
function votingProposal(bool positive) external onlyTrustedForwarder {
address voter = _msgSender();
_votingProposal(positive, voter);
emit ProposalVoted();
}
在此示例中,我们不需要从合约转移费用,因为我们使用的是 Gelato 1Balance。这些更改允许只有可信转发器的地址才能调用该函数,并且(在底层)解码原始交易发送者并通过方法 _msgSender()
使其可用。
现在我们的合约已准备就绪,我们必须更新我们的前端调用合约的方式。
在这里,我们可以看到我们如何调用交易:
/// Vote function
async vote(value: boolean) {
try {
await doSignerTransaction(this.gaslessVoting.votingProposal(value));
} catch (error) {
alert('only one vote per user');
}
}
首先,我们将安装 Gelato Relay SDK
npm i @gelato-network/relay-sdk:
导入 sdk
import { GelatoRelay } from '@gelatonetwork/relay-sdk';
实例化 Gelato Relay 对象
const relay = new GelatoRelay();
按照 [文档] (https://docs.gelato.network/developer-services/relay/quick-start/sponsoredcallerc2771) 构建请求
const request = {
chainId: 5, // Goerli 在这种情况下
target: this.readGaslessVoting.address, // 目标合约地址
data: data!, // 编码的交易数据
user: signerAddress!, // 签名者地址
};
我们的数据将是
const { data } =
await this.gaslessVoting.populateTransaction.votingProposal(value);
最后,我们将使用 SDK 方法 sponsoredCallERC2771
发送我们的请求,传递请求对象、provider 和 1Balance sponsorApiKey
,并检索任务 ID。
const sponsorApiKey = '1NnnocBNgXnG1VgUnFTHXmUICsvYqfjtKsAq1OCmaxk_';
const relayResponse = await relay.sponsoredCallERC2771(
request,
new ethers.providers.Web3Provider(ethereum),
sponsorApiKey
);
let taskId = relayResponse.taskId
我们的 gasless 交易现在看起来像这样
async vote(value: boolean) {
try {
const { data } =
await this.gaslessVoting.populateTransaction.votingProposal(value);
const request = {
chainId: 5, // Goerli 在这种情况下
target: this.readGaslessVoting.address, // 目标合约地址
data: data!, // 编码的交易数据
user: this.dapp.signerAddress!, // 发送交易的用户
};
const sponsorApiKey = '1NnnocBNgXnG1VgUnFTHXmUICsvYqfjtKsAq1OCmaxk_';
const relayResponse = await relay.sponsoredCallERC2771(
request,
new ethers.providers.Web3Provider(ethereum),
sponsorApiKey
);
let taskId = relayResponse.taskId
} catch (error) {
alert('only one vote per user');
}
}
通过这些小的更改,我们将常规交易转换为 gasless 交易,并通过跨链中央余额资助 gas 费用。
在我们的演示应用中,我们实施了 Gelato Automate 以在投票期后(大约 30 分钟后)关闭投票提案。如果你有兴趣在任何合约中集成 Gelato Automate,我们在下面包含了一些方便的代码片段。 Gelato Automate 合约地址可以在 此处 找到。
要继承的 Gelato 合约位于以下 仓库 中。
/ #region ========== ============= GELATO OPS AUTOMATE CLOSING PROPOSAL ============= ============= //
//@dev 创建 gelato 任务
function createFinishVotingTask() internal returns (bytes32 taskId) {
bytes memory timeArgs = abi.encode(
uint128(block.timestamp + proposalValidity),
proposalValidity
);
//@dev 编码的执行函数
bytes memory execData = abi.encodeWithSelector(this.finishVoting.selector);
LibDataTypes.Module[] memory modules = new LibDataTypes.Module[](2);
//@dev 使用在特定时间间隔执行的前缀,并且只执行一次
modules[0] = LibDataTypes.Module.TIME;
modules[1] = LibDataTypes.Module.SINGLE_EXEC;
bytes[] memory args = new bytes[](1);
args[0] = timeArgs;
LibDataTypes.ModuleData memory moduleData = LibDataTypes.ModuleData(
modules,
args
);
//@dev 任务创建
taskId = IOps(ops).createTask(address(this), execData, moduleData, ETH);
}
//@dev 由 Gelato 调用的执行函数
function finishVoting() public onlyOps {
(uint256 fee, address feeToken) = IOps(ops).getFeeDetails();
transfer(fee, feeToken);
}
//@dev 将费用转移到 Gelato
function transfer(uint256 _amount, address _paymentToken) internal {
(bool success, ) = gelato.call{value: _amount}("");
require(success, "_transfer: ETH transfer failed");
}
//@dev only Gelato 修饰符
modifier onlyOps() {
require(msg.sender == address(ops), "OpsReady: onlyOps");
_;
}
// #endregion ========== ============= GELATO OPS AUTOMATE CLOSING PROPOSAL ============= ============= //
Gelato 是一个 Web3 云平台,使开发人员能够创建自动化的、gasless 的和 off-chain-aware 的 Layer 2 链和智能合约。超过 400 个 web3 项目多年来依赖 Gelato 来促进 DeFi、NFT 和游戏中的数百万笔交易。
Gelato RaaS: 一键部署你自己的量身定制的 ZK 或 OP L2 链,内置原生账户抽象和所有 Gelato 中间件。
Web3 Functions: 通过运行去中心化的云函数,将你的智能合约连接到链下数据和计算。
Automate: 通过以可靠、对开发者友好的和去中心化的方式自动执行交易来自动化你的智能合约。
Relay: 通过一个易于使用的 API,让你的用户访问可靠、强大和可扩展的 gasless 交易。
账户抽象 SDK: Gelato 已与 Safe 合作,构建一个完全成熟的账户抽象 SDK,结合 Gelato 行业最佳的 gasless 交易能力与行业最安全的智能合约钱包。
订阅我们的新闻通讯并打开你的 Twitter 通知,以获取有关 Gelato 生态系统的最新更新! 如果你有兴趣成为 Gelato 团队的一员并构建互联网的未来,请浏览空缺职位并在此处申请 这里。
- 原文链接: gelato.cloud/blog/how-to...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!