发送无 Gas 交易 - OpenZeppelin 文档

本文介绍了如何使用 Gas Station Network (GSN) 实现以太坊上的无 gas 交易,允许用户在无需支付 gas 费用的情况下与 DApp 交互。文章涵盖了 meta 交易的概念、GSN 的运作方式,以及如何从头构建一个 GSN 驱动的 DApp,并使用 GSN Starter Kit 快速开始开发。

发送无 gas 交易

本指南现已弃用,因为它使用 GSNv1,该版本已不再受支持。 考虑使用 OpenGSN 团队的 GSNv2 来获得去中心化的解决方案。
本文不再维护。 有关更多信息,请点击此处阅读。

任何发送以太坊交易的人都需要有以太币来支付 gas 费用。 这迫使新用户在开始使用 dapp 之前购买以太币(这可能是一项艰巨的任务)。 这是用户加入的主要障碍。

在本指南中,我们将探讨无 gas(也称为 meta)交易的概念,其中用户无需支付其 gas 费用。 我们还将介绍 Gas Station Network,这是解决此问题的去中心化解决方案,以及 OpenZeppelin 库,这些库使你可以在你的dapp中使用它:

什么是 Meta-transaction?

所有以太坊交易都使用 gas,并且每笔交易的发送者必须有足够的以太币来支付所花费的 gas。 即使这些 gas 成本对于基本交易来说很低(几美分),但获得以太币并非易事:dApp 用户通常需要通过了解你的客户和反洗钱流程(KYC & AML),这不仅需要时间,而且通常涉及在互联网上发送一张手持护照的自拍照(!)。 最重要的是,他们还需要提供财务信息才能通过交易所购买以太币。 只有最铁杆的用户才会忍受这种麻烦,并且当需要以太币时,dApp 的采用会大大受损。 我们可以做得更好。

进入 meta-transaction。 这是对一个简单想法的一个奇特名称:第三方(称为 relayer)可以发送另一个用户的交易并自己支付 gas 成本。 在此方案中,用户签署消息(而非交易),其中包含有关他们想要执行的交易的信息。 然后,Relayer 负责使用此信息签署有效的以太坊交易并将其发送到网络,从而支付 gas 成本。 一个 base contract 保留了最初请求交易的用户的身份。 这样,用户可以直接与智能合约交互,而无需拥有钱包或拥有以太币。

这意味着,为了在你的应用程序中支持 meta transaction,你需要保持 relayer 进程运行 - 或利用去中心化的 relayer 网络。

Gas Station Network

Gas Station Network (GSN) 是一个去中心化的 relayer 网络。 它允许你构建 dapp,你可以在其中支付用户的交易费用,因此他们无需持有以太币来支付 gas 费用,从而简化了他们的加入过程。

GSN 最初由 TabooKey 构思和设计,并且已经发展到包含以太坊领域的许多公司,这些公司希望共同努力以解决用户加入以太坊应用程序的问题。

但是,GSN 中的 relayer 并非在运营慈善机构:他们经营的是一项业务。 他们很乐意为你的用户的 gas 成本付费的原因是因为他们反过来会向你的合约(recipient)收费。 这样,relayer 可以收回他们的钱,再加上一点额外的费用作为他们服务的费用。

乍一看,这可能听起来很奇怪,但为用户加入付费是一种非常常见的商业行为。 大量资金用于广告、免费试用、新用户折扣等,所有这些都以 获取用户为目标。 与这些相比,几个以太坊交易的成本实际上非常小。

此外,你可以在用户提前在链下向你付款(例如,通过信用卡)的情况下利用 GSN,每次 GSN 调用都会从他们系统中的余额中扣除。 有无限的可能!

此外,GSN 的设置方式使得为 relayer 服务你的请求最有利,并且已采取措施来惩罚他们的不当行为。 所有这些都会自动发生,因此你可以安全地开始使用他们的服务,而无需担心。

你可以在 RelayHub 交互 中了解有关 GSN 如何运作的更多信息。

构建一个 GSN 驱动的 DApp

是时候构建一个利用 GSN 的 dapp 并将其推送到测试网了。 在本节中,我们将使用:

这里似乎有很多移动部件,但每个组件在此应用程序的构建中都有明确定义的角色。 也就是说,如果你是 OpenZeppelin 平台的新手,那么在继续阅读之前,查看 OpenZeppelin Contracts GSN 指南构建 Dapp 教程可能会有所帮助。

我们将创建一个简单的合约,该合约仅计算发送给它的交易,但会将其绑定到 GSN,以便用户在发送这些交易时不必支付 gas 费用。 让我们开始吧!

设置环境

我们将首先创建一个新的 npm 项目并安装所有依赖项,包括 Ganache(我们将使用它来 运行本地网络):

$ mkdir gsn-dapp && cd gsn-dapp
$ npm init -y
$ npm install @openzeppelin/network
$ npm install --save-dev @openzeppelin/gsn-helpers @openzeppelin/contracts-ethereum-package @openzeppelin/upgrades @openzeppelin/cli ganache-cli

使用 CLI 设置一个新项目,然后按照提示操作,以便我们可以编写我们的第一个合约。

$ npx oz init
如果你不熟悉它,请查看 OpenZeppelin CLI 入门

创建我们的合约

我们将在新创建的 contracts 文件夹中编写我们的 vanilla Counter 合约。

// contracts/Counter.sol
pragma solidity ^0.5.0;

contract Counter {
    uint256 public value;

    function increase() public {
        value += 1;
    }
}

这很简单。 现在,让我们修改它以添加 GSN 支持。 这需要从 GSNRecipient 合约扩展并实现 acceptRelayedCall 方法。 此方法必须返回我们是否接受或拒绝支付用户交易。 为了简单起见,我们将支付发送到此合约的所有交易。

对于大多数 (d)app,拥有如此慷慨的策略可能不是一个好主意,因为任何恶意用户都可以轻松耗尽你合约的资金。 查看我们的 GSN 支付策略指南,了解解决此问题的不同方法。
// contracts/Counter.sol
pragma solidity ^0.5.0;

import "@openzeppelin/contracts-ethereum-package/contracts/GSN/GSNRecipient.sol";

contract Counter is GSNRecipient {
    uint256 public value;

    function increase() public {
        value += 1;
    }

    function acceptRelayedCall(
        address relay,
        address from,
        bytes calldata encodedFunction,
        uint256 transactionFee,
        uint256 gasPrice,
        uint256 gasLimit,
        uint256 nonce,
        bytes calldata approvalData,
        uint256 maxPossibleCharge
    ) external view returns (uint256, bytes memory) {
        return _approveRelayedCall();
    }

    // We won't do any pre or post processing, so leave _preRelayedCall and _postRelayedCall empty
    // 我们不会进行任何预处理或后处理,因此保持 _preRelayedCall 和 _postRelayedCall 为空
    function _preRelayedCall(bytes memory context) internal returns (bytes32) {
    }

    function _postRelayedCall(bytes memory context, bool, uint256 actualCharge, bytes32) internal {
    }
}

通过运行 npx ganache-cli 在单独的终端上启动 ganache。 然后,使用 OpenZeppelin CLI 和 npx oz create 创建我们的新合约的一个实例,并按照提示进行操作,包括选择调用一个函数来初始化该实例。

请务必记下你的实例的地址,该地址在此过程结束时返回!

请务必记住在创建合约时调用 initialize() 函数,因为这将使你的合约准备好在 GSN 中使用。
$ openzeppelin create
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
? Pick a contract to instantiate Counter
? Pick a network development
All contracts are up to date
? Call a function to initialize the instance after creating it? Yes
? Select which function * initialize()
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601

太棒了! 现在,如果我们将此合约部署到主网或 goerli 测试网,我们将几乎可以开始向其发送无 gas 交易,因为 GSN 已经在这两个网络上设置好了。 但是,由于我们在本地 ganache 上,因此我们需要自己设置。

部署用于开发的本地 GSN

GSN 由一个中央 RelayHub 合约组成,该合约协调所有中继交易,以及多个去中心化的 relayer。 Relayer 是通过 HTTP 接口接收中继交易请求并通过 RelayHub 将其发送到网络的过程。

在 ganache 运行的情况下,你可以使用 OpenZeppelin GSN Helpers 中的以下命令在一个新终端中启动一个 relayer:

$ npx oz-gsn run-relayer
Deploying singleton RelayHub instance
RelayHub deployed at 0xd216153c06e857cd7f72665e0af1d7d82172f494
Starting relayer
 -Url http://localhost:8090
...
RelayHttpServer starting. version: 0.4.0
...
Relay funded. Balance: 4999305160000000000
在底层,此命令会处理多个步骤以使本地 relayer 启动并运行。 首先,它将为你的平台下载一个 relayer 二进制文件并启动它。 然后,它会将 RelayHub 合约部署到你的本地 ganache,在 hub 上注册 relayer 并为其提供资金,以便它可以中继交易。 你可以使用其他 oz-gsn commands 甚至 直接从你的 JavaScript 代码 单独运行这些步骤。

最后一步是 fund 我们的 Counter 合约。 GSN relayer 要求 recipient 合约有资金,因为他们会将中继交易的成本(加上费用!)记入其中。 我们将再次使用 oz-gsn 命令集来执行此操作:

$ npx oz-gsn fund-recipient --recipient 0xCfEB869F69431e42cdB54A4F4f105C19C080A601
确保用你的 Counter 合约实例的地址替换 recipient 地址!

太棒了! 现在我们有了支持 GSN 的合约和一个本地 GSN 来试用它,让我们构建一个小 (d)app。

创建 Dapp

我们将使用 create-react-app 包创建我们的 (d)app,该包使用 React 引导一个简单的客户端应用程序。

$ npx create-react-app client

首先,创建一个符号链接,以便我们可以访问我们编译的合约 .json 文件。 从 client/src 目录中,运行:

$ ln -ns ../../build

这将允许我们的前端访问我们的合约工件。

然后,将 client/src/App.js 替换为以下代码。 这将使用 OpenZeppelin Network JS 创建一个连接到本地网络的新提供商。 它将使用当场生成的密钥代表用户签署所有交易,并将使用 GSN 将它们中继到网络。 这允许你的用户立即开始与你的 (d)app 交互,即使他们没有安装 MetaMask、以太坊账户或任何以太币。

// client/src/App.js
import React, { useState, useEffect, useCallback } from "react";
import { useWeb3Network } from "@openzeppelin/network/react";

const PROVIDER_URL = "http://127.0.0.1:8545";

function App() {
  // get GSN web3
  // 获取 GSN web3
  const context = useWeb3Network(PROVIDER_URL, {
    gsn: { dev: true }
  });

  const { accounts, lib } = context;

  // load Counter json artifact
  // 加载 Counter json 工件
  const counterJSON = require("./build/contracts/Counter.json");

  // load Counter Instance
  // 加载 Counter 实例
  const [counterInstance, setCounterInstance] = useState(undefined);

  if (
    !counterInstance &&
    context &&
    context.networkId
  ) {
    const deployedNetwork = counterJSON.networks[context.networkId.toString()];
    const instance = new context.lib.eth.Contract(counterJSON.abi, deployedNetwork.address);
    setCounterInstance(instance);
  }

  const [count, setCount] = useState(0);

  const getCount = useCallback(async () => {
    if (counterInstance) {
      // Get the value from the contract to prove it worked.
      // 从合约中获取值以证明它有效。
      const response = await counterInstance.methods.value().call();
      // Update state with the result.
      // 使用结果更新状态。
      setCount(response);
    }
  }, [counterInstance]);

  useEffect(() => {
    getCount();
  }, [counterInstance, getCount]);

  const increase = async () => {
    await counterInstance.methods.increase().send({ from: accounts[0] });
    getCount();
  };

  return (
    <div>
      <h3> Counter counterInstance </h3>
      {lib && !counterInstance && (
        <React.Fragment>
          <div>Contract Instance or network not loaded.</div>
        </React.Fragment>
      )}
      {lib && counterInstance && (
        <React.Fragment>
          <div>
            <div>Counter Value:</div>
            <div>{count}</div>
          </div>
          <div>Counter Actions</div>
            <button onClick={() => increase()} size="small">
              Increase Counter by 1
            </button>
        </React.Fragment>
      )}
    </div>
  );
}

export default App;
你可以在设置提供程序时将 dev: true 标志传递给 gsn 选项。 这将使用 GSNDevProvider 而不是常规 GSN 提供程序。 这是一个专门为测试或开发设置的提供程序,它 不需要 relayer 运行 才能工作。 这可以简化开发,但会感觉不太像实际的 GSN 体验。 如果你想使用实际的 relayer,你可以在本地运行 npx oz-gsn run-relayer(有关更多信息,请参阅 准备测试环境)。

太棒了! 我们现在可以从 client 文件夹中运行 npm start 来启动我们的应用程序。 记住要保持你的 ganache 和 relayer 启动并运行。 你应该能够将交易发送到你的 Counter 合约,而无需使用 MetaMask 或拥有任何 ETH!

转移到测试网

在你的 ganache 网络中发送本地交易并不是很令人印象深刻,因为你已经有很多资金充足的账户。 为了充分发挥 GSN 的潜力,让我们将我们的应用程序转移到 goerli 测试网。 如果你以后想进入主网,说明是相同的。

你需要在 networks.js 文件中创建一个新条目,其中包含一个已获得资金的 goerli 帐户。 有关如何执行此操作的详细说明,请查看 部署到公共测试网络

我们现在可以将我们的 Counter 合约部署到 goerli:

$ openzeppelin create
✓ Compiled contracts with solc 0.5.9 (commit.e560f70d)
? Pick a contract to instantiate: Counter
? Pick a network: goerli
✓ Added contract Counter
✓ Contract Counter deployed
? Call a function to initialize the instance after creating it?: Yes
? Select which function * initialize()
✓ Setting everything up to create contract instances
✓ Instance created at 0xCfEB869F69431e42cdB54A4F4f105C19C080A601

下一步是指示我们的 (d)app 连接到 goerli 节点而不是本地网络。 将你的 App.js 中的 PROVIDER_URL 更改为例如 Infura goerli 端点。

我们现在将使用真正的 GSN 提供程序而不是我们的开发人员环境,因此你可能还想提供一个 配置对象,它将使你能够更好地控制你愿意支付的 gas 价格等。 对于生产 (d)app,你将需要根据你的要求配置此设置。

import { useWeb3Network, useEphemeralKey } from "@openzeppelin/network/react";

// inside App.js#App()
// 在 App.js#App() 内部
const context = useWeb3Network('https://goerli.infura.io/v3/' + INFURA_API_TOKEN, {
  gsn: { signKey: useEphemeralKey() }
});

我们快到了! 如果你现在尝试使用你的 (d)app,你会注意到你无法发送任何交易。 这是因为你的 Counter 合约尚未在此网络上获得资金。 我们现在将使用 online gsn-tool,而不是使用我们之前使用的 oz-gsn fund-recipient 命令,方法是粘贴你的实例的地址。 为此,Web 界面要求你在 goerli 网络上使用 MetaMask,这将允许你将资金存入你的合约。

OpenZeppelin GSN Dapp Tool

就这样! 我们现在可以从我们的浏览器开始向我们在 goerli 网络上的 Counter 合约发送交易,甚至无需安装 MetaMask。

GSN Starter Kit

Starter Kit 是预配置的项目模板,用于引导 dapp 开发。 其中一个,GSN Starter Kit,是一个即用型 dapp,它连接到 GSN,其设置与我们在上一节中从头构建的类似。

如果你正在构建一个新的 dapp 并且想要包含 meta-transaction 支持,你可以运行 oz unpack gsn 来快速启动你的开发并从启用 GSN 的盒子开始!

接下来步骤

要了解有关 GSN 的更多信息,请前往以下资源:

  • 原文链接: docs.openzeppelin.com/le...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
OpenZeppelin
OpenZeppelin
江湖只有他的大名,没有他的介绍。