如何创建你的第一个 Gasless 应用

  • gelato
  • 发布于 2023-03-08 22:54
  • 阅读 33

本文介绍了如何使用 Gelato Relay 为智能合约启用 gasless 交易,从而构建为用户提供 gasless 体验的应用程序。通过一个 DAO 提案投票的示例应用,展示了如何将 createProposal() 和 vote() 交易转换为 gasless 交易,并使用 Gelato Automate 自动关闭投票提案。

简介

摘要:

  • Gasless 交易是指从正常的钱包流程(例如 Metamask)中抽象出 gas 支付的交易,允许你的用户在没有网络原生代币的情况下发送签名交易
  • Gelato Relay 启用了元交易,因此用户无需支付 gas 即可与你的智能合约交互
  • 本文将教你如何为你的智能合约启用 gasless 交易,从而构建能够为用户提供 gasless 体验的应用

演示应用:DAOs 的 Gasless 提案投票

我们将从一个允许 DAO 成员发送提案的应用开始。一旦提案被发送,用户有 30 分钟的时间进行投票,之后一个自动化的 Gelato 任务将结束投票期。

一开始不是 Gasless 的

最初,每个提案和投票都是签名交易。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 节点中,我们可以通过创建提案和投票来进行测试。稍后,我们将部署到测试网来测试中继的交易,因为这涉及到链下基础设施。

为什么 Gasless?

许多 DAO 都在努力解决的一个问题是成员参与度,尤其是当成员由于 gas 限制而难以对提案进行投票时。为了消除投票支付 gas 的摩擦,我们将把我们的合约转换成一个 relay-aware 的合约。但首先,让我们深入了解一下 relayer 的工作原理gelaro-relay.png

  • 该应用借助 Gelato Relay-SDK 向 Gelato 发送一个 HTTP post 请求
  • Gelato 将请求负载转发到 Gelato Relay 合约
  • 目标合约执行该交易

要实现 relayer,我们需要决定如何资助交易,以及是否需要 Gelato 的支持来验证用户身份。

如何一步一步实现 Gasless

以下是我们简化流程的方法:DAO 可以轻松创建一个应用,允许用户创建提案和投票,而无需支付 gas。

我们将把 createProposal()vote() 交易转换为 gasless 交易。

按照下表,我们将确定要使用哪个合约和 SDK 方法。

Gelato 认证 付款方式 继承合约 SDK/API 方法
用户 GelatoRelayContext relayWithSyncFee
用户 GelatoRelayContextERC2771 relayWithSyncFeeERC2771
1Balance n. a. relayWithSponsoredCall
是¹ 1Balance ERC2771Context relayWithSponsoredCallERC2771
  1. 需要 SponsorKey;请在此处访问 Gelato 1Balance 这里

没有身份验证和 1Balance 的交易 (relayWithSyncFee)

在这种情况下,我们允许所有用户创建提案,因此无需验证用户身份。通常我们建议使用 1Balance 作为付款方式,但对于本演示,我们将使用 Gelato Relay 的 SyncFee 付款方式。

如果我们按照上表,我们位于第一行,不需要验证用户身份。

  • 继承合约:GelatoRelayContext
  • Sdk 方法:relayWithSyncFee

智能合约更新

我们当前的 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 交易。如果我们深入了解一下,我们可以看到应用的更改:

  • 确保只有 Gelato Relay 合约可以调用 createProposal() 方法
  • 解码附加到 calldata 的费用、费用收集器和 feeToken,并将其用于将费用转移到 Gelato

前端更新 (SDK)

现在我们的合约已准备就绪,我们必须更新我们的前端调用合约的方式。

在这里,我们可以看到我们到目前为止如何调用交易:

 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 的交易 (relayWithSponsoredCallERC2771)

现在假设我们的应用希望每个用户只有一个投票。在这种情况下,我们将需要验证用户身份,并且我们将使用 1Balance 来赞助我们的交易。

如果我们再次按照方便的表格,此用例与最后一行匹配:

  • 继承合约:ERC2771Context
  • Sdk 方法:relayWithSponsoredCallERC2771

配置 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() 使其可用。

前端更新 (SDK)

现在我们的合约已准备就绪,我们必须更新我们的前端调用合约的方式。

在这里,我们可以看到我们如何调用交易:

 /// 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

在我们的演示应用中,我们实施了 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

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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
gelato
gelato
The Web3 Developer Cloud. Launch your own chain via our #1 Rollup-As-A-Service platform.