本文介绍了如何使用 Gas Station Network (GSN) 实现以太坊上的无 gas 交易,允许用户在无需支付 gas 费用的情况下与 DApp 交互。文章涵盖了 meta 交易的概念、GSN 的运作方式,以及如何从头构建一个 GSN 驱动的 DApp,并使用 GSN Starter Kit 快速开始开发。
本指南现已弃用,因为它使用 GSNv1,该版本已不再受支持。 考虑使用 OpenGSN 团队的 GSNv2 来获得去中心化的解决方案。 |
本文不再维护。 有关更多信息,请点击此处阅读。 |
任何发送以太坊交易的人都需要有以太币来支付 gas 费用。 这迫使新用户在开始使用 dapp 之前购买以太币(这可能是一项艰巨的任务)。 这是用户加入的主要障碍。
在本指南中,我们将探讨无 gas(也称为 meta)交易的概念,其中用户无需支付其 gas 费用。 我们还将介绍 Gas Station Network,这是解决此问题的去中心化解决方案,以及 OpenZeppelin 库,这些库使你可以在你的dapp中使用它:
所有以太坊交易都使用 gas,并且每笔交易的发送者必须有足够的以太币来支付所花费的 gas。 即使这些 gas 成本对于基本交易来说很低(几美分),但获得以太币并非易事:dApp 用户通常需要通过了解你的客户和反洗钱流程(KYC & AML),这不仅需要时间,而且通常涉及在互联网上发送一张手持护照的自拍照(!)。 最重要的是,他们还需要提供财务信息才能通过交易所购买以太币。 只有最铁杆的用户才会忍受这种麻烦,并且当需要以太币时,dApp 的采用会大大受损。 我们可以做得更好。
进入 meta-transaction。 这是对一个简单想法的一个奇特名称:第三方(称为 relayer)可以发送另一个用户的交易并自己支付 gas 成本。 在此方案中,用户签署消息(而非交易),其中包含有关他们想要执行的交易的信息。 然后,Relayer 负责使用此信息签署有效的以太坊交易并将其发送到网络,从而支付 gas 成本。 一个 base contract 保留了最初请求交易的用户的身份。 这样,用户可以直接与智能合约交互,而无需拥有钱包或拥有以太币。
这意味着,为了在你的应用程序中支持 meta transaction,你需要保持 relayer 进程运行 - 或利用去中心化的 relayer 网络。
Gas Station Network (GSN) 是一个去中心化的 relayer 网络。 它允许你构建 dapp,你可以在其中支付用户的交易费用,因此他们无需持有以太币来支付 gas 费用,从而简化了他们的加入过程。
GSN 最初由 TabooKey 构思和设计,并且已经发展到包含以太坊领域的许多公司,这些公司希望共同努力以解决用户加入以太坊应用程序的问题。 |
但是,GSN 中的 relayer 并非在运营慈善机构:他们经营的是一项业务。 他们很乐意为你的用户的 gas 成本付费的原因是因为他们反过来会向你的合约(recipient)收费。 这样,relayer 可以收回他们的钱,再加上一点额外的费用作为他们服务的费用。
乍一看,这可能听起来很奇怪,但为用户加入付费是一种非常常见的商业行为。 大量资金用于广告、免费试用、新用户折扣等,所有这些都以 获取用户为目标。 与这些相比,几个以太坊交易的成本实际上非常小。
此外,你可以在用户提前在链下向你付款(例如,通过信用卡)的情况下利用 GSN,每次 GSN 调用都会从他们系统中的余额中扣除。 有无限的可能!
此外,GSN 的设置方式使得为 relayer 服务你的请求最有利,并且已采取措施来惩罚他们的不当行为。 所有这些都会自动发生,因此你可以安全地开始使用他们的服务,而无需担心。
你可以在 与 RelayHub 交互 中了解有关 GSN 如何运作的更多信息。 |
是时候构建一个利用 GSN 的 dapp 并将其推送到测试网了。 在本节中,我们将使用:
create-react-app
包来引导一个 React 应用程序,以及 OpenZeppelin Network JS 来轻松设置支持 GSN 的 web3 对象
OpenZeppelin GSN Helpers 在你的本地 ganache 实例中模拟 GSN
@openzeppelin/contracts-ethereum-package
智能合约库来获取 GSN
OpenZeppelin CLI 来管理和部署我们的合约
这里似乎有很多移动部件,但每个组件在此应用程序的构建中都有明确定义的角色。 也就是说,如果你是 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 由一个中央 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。
我们将使用 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,这将允许你将资金存入你的合约。
就这样! 我们现在可以从我们的浏览器开始向我们在 goerli 网络上的 Counter
合约发送交易,甚至无需安装 MetaMask。
Starter Kit 是预配置的项目模板,用于引导 dapp 开发。 其中一个,GSN Starter Kit,是一个即用型 dapp,它连接到 GSN,其设置与我们在上一节中从头构建的类似。
如果你正在构建一个新的 dapp 并且想要包含 meta-transaction 支持,你可以运行 oz unpack gsn
来快速启动你的开发并从启用 GSN 的盒子开始!
要了解有关 GSN 的更多信息,请前往以下资源:
要了解如何使用 OpenZeppelin Contracts 构建支持 GSN 的合约,请前往 GSN 基础知识指南。
如果你想学习如何使用 OpenZeppelin Contracts 的 预制接受和收费策略,请转到 GSN 策略指南。
如果你想了解更多关于如何从你的应用程序中使用 GSN 的信息,请前往 OpenZeppelin GSN Provider 指南。
有关如何测试启用 GSN 的合约 的信息,请转到 OpenZeppelin GSN Helpers 文档。
- 原文链接: docs.openzeppelin.com/le...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!