如何构建一个DApp:完整的DApp架构¶
作者:Jeremy Boetticher
概览¶
去中心化应用(DApp)重新定义了应用程序在Web3中的构建、管理和交互方式。通过利用区块链技术,DApp提供了一个安全、透明且无需信任的系统,在无需任何中央授权的情况下即可实现点对点交互。DApp架构的核心由几个主要组件组成,它们协同工作以创建一个强大且去中心化的生态系统。这些组件包括智能合约、节点、前端用户界面等。
在本教程中,您将通过编写一个用以铸造Token的完整DApp来面对面了解每个主要组件。我们还将探索DApp的其他可选组件,这些组件可以增强您未来项目的用户体验。您可以在GitHub上的monorepo中查看完整的项目内容。
查看先决条件¶
要开始进行操作,您需要具备以下条件:
- 一个拥有DEV的Moonbase Alpha账户 您可以每24小时一次从Moonbase Alpha水龙头上获取DEV代币以在Moonbase Alpha上进行测试
- 已安装版本16或是以上的Node.js
- VS Code与Juan Blanco的Solidity扩展是推荐的IDE
- JavaScript和React的相关背景知识
- 对于Solidity的基础了解。如果您并不熟悉Solidity,网络上有很多相关资源,包含Solidity范例教程。大约15分钟的快速学习即可用于本教程之中
- 已安装类似于MetaMask的钱包
节点和JSON-RPC端点¶
一般来说,JSON-RPC是一种利用JSON对数据进行编码的远程过程调用(RPC)协议。在Web3产业中,它们指的是DApp开发者用来发送请求和接收来自区块链节点响应的特定JSON-RPC,这让它成为与智能合约交互的关键元素。它们允许前端用户界面与智能合约无缝交互,并为用户提供有关其操作的实时反馈。它们还允许开发者先部署他们的智能合约!
要让JSON-RPC与Moonbeam区块链进行通信,您需要运行一个节点。但这可能是昂贵、复杂且麻烦的。幸运的是,只要您有权访问节点,就可以与区块链进行交互。Moonbase Alpha有一些免费和付费节点选项。在本教程中,我们将使用Moonbeam基金会的Moonbase Alpha公共节点,但建议您获取自己的私有端点。
https://rpc.api.moonbase.moonbeam.network
现在您有了一个URL。您会如何使用它?通过HTTPS
,JSON-RPC请求是POST
请求,其中包括用于读取和写入数据的特定函数,例如用于以只读方式执行智能合约功能的eth_call
或用于将签名交易提交到的网络eth_sendRawTransaction
(为改变区块链状态的调用)。整个JSON请求结构将始终具有类似于以下的结构:
{
"jsonrpc": "2.0",
"id": 1,
"method": "eth_getBalance",
"params": ["0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac", "latest"]
}
此范例为使用0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac
账户的余额(在Moonbase Alpha中为DEV)。让我们解析其中的元素:
jsonrpc
— JSON-RPC APR版本,通常为“2.0”id
— 用于定义请求回应的常数值,通常可以保持为method
— 特定从/至区块链读/写数据的函数。您可以在我们的文档网站查看许多不同的RPC函数params
— 根据特定method
的输入参数阵列
事实上还有其他额外的元素会被加入至JSON-RPC请求中,但这四个最为常见。
在当前,这些JSON-RPC请求非常有用,但是在编写代码时,一遍又一遍地创建JSON对象可能会很麻烦。这就是为什么存在有助于抽象和促进这些请求的使用的库。Moonbeam提供了许多库的文档,我们将在本教程中使用Ethers.js。您仅需要了解,每当我们通过Ethers.js包与区块链交互时,我们实际上是在使用 JSON-RPC!
智能合约¶
智能合约是自动执行的合约,协议条款将会直接被写入代码中。它们充当任何DApp的去中心化后端,自动化并强制执行系统内的商业逻辑。
如果您来自传统的Web开发背景,智能合约的用意旨在取代后端,但需要注意的是:用户必须拥有原生Token(GLMR、MOVR、DEV等)才能发出状态更改请求,存储信息可能会很昂贵,并且存储的信息均不为仅自己可见。
当您将智能合约部署到Moonbeam上时,您会上传一系列EVM或以太坊虚拟机可以理解的指令。每当有人与智能合约交互时,EVM都会执行这些透明、防篡改且不可变的指令来更改区块链的状态。在智能合约中正确编写指令非常重要,因为区块链的状态定义了有关DApp的最关键信息,例如谁拥有多少资金金额。
由于指令在低(组合)级别上很难编写和理解,因此我们使用Solidity等智能合约语言来简化它们的编写。为了帮助编写、调试、测试和编译这些智能合约语言,以太坊社区的开发者创建了开发者环境,例如Hardhat和Foundry。Moonbeam的开发者网站提供了有关大量开发者环境的信息。
本教程将会使用Hardhat管理智能合约。
创建一个Hardhat项目¶
您可以使用以下指令在Hardhat上发起项目:
npx hardhat init
创建JavaScript或TypeScript Hardhat项目时,系统会询问您是否要安装示例项目的依赖项,即为安装Hardhat和Hardhat Toolbox插件。您不需要工具箱中包含的所有插件,因此您可以安装Hardhat、Ethers和Hardhat Ethers插件,这就是本教程所需的全部内容:
npm install --save-dev hardhat @nomicfoundation/hardhat-ethers ethers@6
在我们开始编写智能合约之前,让我们先将JSON-RPC URL添加至配置之中,我们可以使用以下代码设置hardhat.config.js
文件,并将INSERT_YOUR_PRIVATE_KEY
取代为您具有资金账户的私钥。
注意事项
这仅用于测试目的,请勿将您具有真实资金的账户私钥以文字的方式储存。
require('@nomicfoundation/hardhat-ethers');
module.exports = {
solidity: '0.8.20',
networks: {
moonbase: {
url: 'https://rpc.api.moonbase.moonbeam.network',
chainId: 1287,
accounts: ['INSERT_YOUR_PRIVATE_KEY']
}
}
};
编写智能合约¶
本教程的目的是在创建一个允许您以一个价格铸造Token的DApp,让我们在这部分编写关于此功能的智能合约!
当您已经发起一个Hardhat项目,您将能够在其contracts
文件夹编写智能合约。其文件夹中拥有一个初始的智能合约,被称为Lock.sol
,但您需要删除它并添加一个被称为MintableERC20.sol
的智能文件。
Token的标准称为ERC-20,其中ERC代表“以太坊请求评论”。很久以前,这个标准就被定义了,现在大多数与Token配合使用的智能合约都期望Token具有ERC-20定义的所有功能。幸运的是,您不必凭记忆知道它,因为OpenZeppelin智能合约团队为我们提供了可供使用的智能合约基础。
npm install @openzeppelin/contracts
现在,在您的MintableERC20.sol
中添加以下代码:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MintableERC20 is ERC20, Ownable {
constructor(address initialOwner) ERC20("Mintable ERC 20", "MERC") Ownable(initialOwner) {}
}
在编写智能合约时,您最终需要将他们进行编译。任何针对智能合约的开发环境皆有这个功能,在Hardhat中,您可以使用以下代码编译:
npx hardhat compile
在此应当能够顺利编译,将会由两个新的文件夹出现,分别为artifacts
和cache
。这两个文件夹存储了编译智能合约的信息。
让我们继续添加功能。您可以将以下常数、错误、事件和功能添加至您的Solidity文件:
uint256 public constant MAX_TO_MINT = 1000 ether;
event PurchaseOccurred(address minter, uint256 amount);
error MustMintOverZero();
error MintRequestOverMax();
error FailedToSendEtherToOwner();
/**Purchases some of the token with native currency. */
function purchaseMint() payable external {
// Calculate amount to mint
uint256 amountToMint = msg.value;
// Check for no errors
if(amountToMint == 0) revert MustMintOverZero();
if(amountToMint + totalSupply() > MAX_TO_MINT) revert MintRequestOverMax();
// Send to owner
(bool success, ) = owner().call{value: msg.value}("");
if(!success) revert FailedToSendEtherToOwner();
// Mint to user
_mint(msg.sender, amountToMint);
emit PurchaseOccurred(msg.sender, amountToMint);
}
MintableERC20.sol文档
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MintableERC20 is ERC20, Ownable {
constructor(address initialOwner) ERC20("Mintable ERC 20", "MERC") Ownable(initialOwner) {}
uint256 public constant MAX_TO_MINT = 1000 ether;
event PurchaseOccurred(address minter, uint256 amount);
error MustMintOverZero();
error MintRequestOverMax();
error FailedToSendEtherToOwner();
/**Purchases some of the token with native gas currency. */
function purchaseMint() external payable {
// Calculate amount to mint
uint256 amountToMint = msg.value;
// Check for no errors
if (amountToMint == 0) revert MustMintOverZero();
if (amountToMint + totalSupply() > MAX_TO_MINT)
revert MintRequestOverMax();
// Send to owner
(bool success, ) = owner().call{value: msg.value}("");
if (!success) revert FailedToSendEtherToOwner();
// Mint to user
_mint(msg.sender, amountToMint);
emit PurchaseOccurred(msg.sender, amountToMint);
}
}
此函数将允许用户传送原生Moonbeam currency(如GLMR、MOVR或DEV)作为价值,因为其为可支付函数。让我们根据不同部分解析此函数。
- 这将会根据传送的价值返还该铸造多少Token
- 接着它会检查铸造数量是否为0或是总铸造数量是否超过
MAX_TO_MINT
,并在两种情况中回传错误描述 - 合约接着会传送函数调用包含其中的数据至合约的所有者(默认为部署合约的地址,也就是您)
- 最后,Token将会铸造给用户,一个事件将会在其后发起
为确保所有流程顺利,让我们再次使用Hardhat:
npx hardhat compile
您现在已经完成您DApp的智能合约!如果这是一个生产应用,我们将会为其编写测试,但这并不包含在本教程的范围中。让我们接着进行部署。
部署智能合约¶
从本质上讲,Harhat是一个Node项目,它使用Ethers.js库与区块链进行交互。您还可以将Ethers.js与Hardhat的工具结合使用来创建脚本执行部署合约等操作。
您的Hardhat项目应当在文件夹中包含scripts
脚本,被称为deploy.js
。让我们以一个更加简单的脚本取代他。
const hre = require('hardhat');
async function main() {
const [deployer] = await hre.ethers.getSigners();
const MintableERC20 = await hre.ethers.getContractFactory('MintableERC20');
const token = await MintableERC20.deploy(deployer.address);
await token.waitForDeployment();
// Get and print the contract address
const myContractDeployedAddress = await token.getAddress();
console.log(`Deployed to ${myContractDeployedAddress}`);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});
该脚本使用Hardhat的Ethers库实例来获取我们之前编写的MintableERC20.sol
智能合约的合约工厂。然后部署它并打印生成的智能合约的地址。使用Hardhat和Ethers.js库非常简单,但仅使用JSON-RPC就困难得多!
让我们在Moonbase Alpha上运行合约(其JSON-RPC端点先前在hardhat.config.js
脚本中定义):
npx hardhat run scripts/deploy.js --network moonbase
您应当能见到展示Token地址的输出,确保您将其保存下来用于之后的教程中!
挑战
Hardhat的内置智能合约部署解决方案并不优秀,它不会自动保存与部署相关的交易和地址!这就是创建hardhat-deploy包的原因。您能自己实现吗?或者您可以切换到不同的开发环境,例如Foundry?
创建一个DApp前端¶
前端为用户提供与基于区块链应用交互的界面。 React是一种用于构建用户界面的流行JavaScript库,由于其基于组件的架构,可促进可重用代码和高效渲染,因此通常用于开发DApp前端。 useDApp包是一个为DApp设计基于Ethers.js的React框架,通过提供一套全面的钩子和组件来简化构建DApp前端的过程以及以太坊区块链功能的集成。
注意事项
一般来说,一个大型项目需要为他们的前端和智能合约在GitHub创建单独的库,但有些小型项目能够创建一个monorepo。
通过useDapp创建一个React项目¶
让我们设置一个新的React项目并安装依赖项,这可以在没有问题的情况下在我们的Hardhat项目中创建。create-react-app
包将会为我们创建新的frontend
目录:
npx create-react-app frontend
cd frontend
npm install ethers@5.6.9 @usedapp/core @mui/material @mui/system @emotion/react @emotion/styled
如果您还记得的话,Ethers.js是一个协助JSON-RPC通信的库。useDApp包是一个类似的库,它使用Ethers.js并将其格式化为React hooks,以便它们在前端项目中更好地工作。我们还添加了两个用于样式和组件的MUI包。
接下来,让我们设置位于frontend/src
目录中的App.js
文件以添加一些视觉结构:
import { useEthers } from '@usedapp/core';
import { Button, Grid, Card } from '@mui/material';
import { Box } from '@mui/system';
const styles = {
box: { minHeight: '100vh', backgroundColor: '#1b3864' },
vh100: { minHeight: '100vh' },
card: { borderRadius: 4, padding: 4, maxWidth: '550px', width: '100%' },
alignCenter: { textAlign: 'center' },
};
function App() {
return (
<Box sx={styles.box}>
<Grid
container
direction='column'
alignItems='center'
justifyContent='center'
style={styles.vh100}
>
{/* This is where we'll be putting our functional components! */}
</Grid>
</Box>
);
}
export default App;
您可以通过在frontend
库中运行以下指令开始React项目:
npm run start
注意事项
此时,您可能会看到几个编译警告,但随着我们继续构建DApp,我们将进行更改以解决这些警告。
您的前端将在localhost:3000上可用。
至此,我们的前端项目已经设置得足够好,可以开始处理功能代码了!
提供者、签名者和钱包¶
前端使用JSON-RPC与区块链通信,但我们将使用Ethers.js。当使用JSON-RPC时,Ethers.js喜欢将与区块链的交互程度抽象为对象,例如提供者、签名者和钱包。
提供者是前端用户界面和区块链网络之间的桥梁,促进通信和数据交换。它们抽象了与区块链交互的复杂性,提供了一个简单的API供前端使用。它们负责将DApp连接到特定的区块链节点,允许其从区块链读取数据,并且本质上包含JSON-RPC URL。
签名者是一种提供者,包含可用于签署交易的秘密。这允许前端创建交易,对其进行签名,然后使用eth_sendRawTransaction
发送它们。签名者有多种类型,但我们最感兴趣的是钱包对象,它安全地存储和管理用户的私钥和数字资产。MetaMask等钱包通过通用且用户友好的流程促进交易签名。它们充当DApp中用户的代表,确保仅执行授权的交易。Ethers.js钱包对象代表我们前端代码中的此接口。
通常,使用Ethers.js的前端将要求您创建一个提供者,连接到用户的钱包(如适用),并创建一个钱包对象。在较大的项目中,这个过程可能会变得难以处理,尤其是在MetaMask之外还存在大量钱包的情况下。
MetaMask难以处理的示例
// Detect if the browser has MetaMask installed
let provider, signer;
if (typeof window.ethereum !== 'undefined') {
// Create a provider using MetaMask
provider = new ethers.BrowserProvider(window.ethereum);
// Connect to MetaMask
async function connectToMetaMask() {
try {
// Request access to the user's MetaMask account
const accounts = await window.ethereum.request({
method: 'eth_requestAccounts',
});
// Create a signer (wallet) using the provider
signer = provider.getSigner(accounts[0]);
} catch (error) {
console.error('Error connecting to MetaMask:', error);
}
}
// Call the function to connect to MetaMask
connectToMetaMask();
} else {
console.log('MetaMask is not installed');
}
// ... also the code for disconnecting from the site
// ... also the code that handles other wallets
幸运的是,我们安装了useDApp包,这为我们简化了许多流程。这同时也抽象了以太坊正在做的事情,这就是为什么我们在这里花了一些时间来解释它们。
创建一个提供者¶
让我们对useDApp包进行一些设置。首先,在React前端的index.js
文件(位于frontend/src
目录中)中,添加一个DAppProvider
对象及其配置。这本质上充当Ethers.js提供程序对象,但可以通过useDApp hook在整个项目中使用:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import { DAppProvider, MoonbaseAlpha } from '@usedapp/core';
import { getDefaultProvider } from 'ethers';
const config = {
readOnlyChainId: MoonbaseAlpha.chainId,
readOnlyUrls: {
[MoonbaseAlpha.chainId]: getDefaultProvider(
'https://rpc.api.moonbase.moonbeam.network'
),
},
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<DAppProvider config={config}>
<App />
</DAppProvider>
</React.StrictMode>
);
连接至钱包¶
现在,在您的App.js
文件中,添加一个允许我们连接到MetaMask的按钮。幸运的是,我们不必编写任何特定于钱包的代码,因为useDApp通过useEthers
hook为我们完成了这件事。
function App() {
const { activateBrowserWallet, deactivate, account } = useEthers();
// Handle the wallet toggle
const handleWalletConnection = () => {
if (account) deactivate();
else activateBrowserWallet();
};
return (
<Box sx={styles.box}>
<Grid
container
direction='column'
alignItems='center'
justifyContent='center'
style={styles.vh100}
>
<Box position='absolute' top={8} right={16}>
<Button variant='contained' onClick={handleWalletConnection}>
{account
? `Disconnect ${account.substring(0, 5)}...`
: 'Connect Wallet'}
</Button>
</Box>
</Grid>
</Box>
);
};
现在屏幕右上角应该有一个按钮将您的钱包连接到您的前端!接下来,让我们了解如何从智能合约中读取数据。
从智能合约中读取数据¶
只要我们知道我们想要读取什么,读取合约非常容易。对于我们的应用,我们将读取可以铸造的最大Token数量以及已铸造的Token数量。通过这种方式,我们可以向用户显示仍然可以铸造多少Token,并希望引起一些FOMO的注意……
如果您只是使用JSON-RPC,您可以使用eth_call
来获取此数据,但这样做非常困难,因为您必须对您的请求进行编码采用称为ABI编码的非直接方法。幸运的是,Ethers.js允许我们轻松创建以人类可读的方式表示合约的对象,只要我们拥有合约的ABI。我们在Hardhat项目的artifacts
目录中拥有MintableERC20.sol
合约的ABI、MintableERC20.json
!
因此,让我们首先将MintableERC20.json
文件移动到我们的前端目录中。每次更改并重新编译智能合约时,您还必须更新前端中的ABI。有些项目的开发者设置会自动从同一源提取ABI,但在这种情况下,我们只需将其复制过来:
|--artifacts
|--@openzeppelin
|--build-info
|--contracts
|--MintableERC20.sol
|--MintableERC20.json // This is the file you're looking for!
...
|--cache
|--contracts
|--frontend
|--public
|--src
|--MintableERC20.json // Copy the file to here!
...
...
...
现在我们有了ABI,我们可以使用它来创建MintableERC20.sol
的合约实例,我们将用它来检索Token数据。
创建一个智能合约实例¶
让我们在App.js
中导入JSON文件和Ethers Contract
对象。我们可以使用地址和ABI创建一个合约对象实例,因此将INSERT_CONTRACT_ADDRESS
替换为您部署时复制的合约地址:
// ... other imports
import MintableERC20 from './MintableERC20.json';
import { Contract } from 'ethers';
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
function App() {
const contract = new Contract(contractAddress, MintableERC20.abi);
// ...
}
App.js文档
import { useEthers } from '@usedapp/core';
import { Button, Grid, Card } from '@mui/material';
import { Box } from '@mui/system';
import { Contract } from 'ethers';
import MintableERC20 from './MintableERC20.json';
const styles = {
box: { minHeight: '100vh', backgroundColor: '#1b3864' },
vh100: { minHeight: '100vh' },
card: { borderRadius: 4, padding: 4, maxWidth: '550px', width: '100%' },
alignCenter: { textAlign: 'center' },
};
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
function App() {
const contract = new Contract(contractAddress, MintableERC20.abi);
const { activateBrowserWallet, deactivate, account } = useEthers();
// Handle the wallet toggle
const handleWalletConnection = () => {
if (account) deactivate();
else activateBrowserWallet();
};
return (
<Box sx={styles.box}>
<Grid
container
direction='column'
alignItems='center'
justifyContent='center'
style={styles.vh100}
>
<Box position='absolute' top={8} right={16}>
<Button variant='contained' onClick={handleWalletConnection}>
{account
? `Disconnect ${account.substring(0, 5)}...`
: 'Connect Wallet'}
</Button>
</Box>
</Grid>
</Box>
);
}
export default App;
与合约接口交互以读取供应数据¶
让我们在新的SupplyComponent.js
文件中创建一个新的SupplyComponent
,它将使用合约接口来检索Token供应数据并显示它:
import { useCall } from '@usedapp/core';
import { utils } from 'ethers';
import { Grid } from '@mui/material';
export default function SupplyComponent({ contract }) {
const totalSupply = useCall({ contract, method: 'totalSupply', args: [] });
const maxSupply = useCall({ contract, method: 'MAX_TO_MINT', args: [] });
const totalSupplyFormatted = totalSupply
? utils.formatEther(totalSupply.value.toString())
: '...';
const maxSupplyFormatted = maxSupply
? utils.formatEther(maxSupply.value.toString())
: '...';
const centeredText = { textAlign: 'center' };
return (
<Grid item xs={12}>
<h3 style={centeredText}>
Total Supply: {totalSupplyFormatted} / {maxSupplyFormatted}
</h3>
</Grid>
);
}
请注意,该组件使用useDApp包提供的useCall
hook。此调用接受我们之前创建的合约对象、字符串函数以及只读调用的任何相关参数,并返回输出。虽然它需要一些设置,但这一行比我们在不使用Ethers.js和useDApp时必须执行的整个use_call
RPC调用要简单得多。
另外请注意,我们使用名为formatEther
的实用程序格式来格式化输出值,而不是直接显示它们。这是因为我们的Token和Gas货币一样,存储为无符号整数,小数点固定为18位。实用函数有助于将该值格式化为我们所期望的方式。
现在我们可以为我们的前端增添趣味并调用合约中的只读函数。我们将更新前端,以便我们有地方显示我们的供应数据:
// ... other imports
import SupplyComponent from './SupplyComponent';
function App() {
// ...
return (
{/* Wrapper Components */}
{/* Button Component */}
<Card sx={styles.card}>
<h1 style={styles.alignCenter}>Mint Your Token!</h1>
<SupplyComponent contract={contract} />
</Card>
{/* Wrapper Components */}
)
}
App.js文档
import { useEthers } from '@usedapp/core';
import { Button, Grid, Card } from '@mui/material';
import { Box } from '@mui/system';
import { Contract } from 'ethers';
import MintableERC20 from './MintableERC20.json';
import SupplyComponent from './SupplyComponent';
const styles = {
box: { minHeight: '100vh', backgroundColor: '#1b3864' },
vh100: { minHeight: '100vh' },
card: { borderRadius: 4, padding: 4, maxWidth: '550px', width: '100%' },
alignCenter: { textAlign: 'center' },
};
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
function App() {
const { activateBrowserWallet, deactivate, account } = useEthers();
const contract = new Contract(contractAddress, MintableERC20.abi);
// Handle the wallet toggle
const handleWalletConnection = () => {
if (account) deactivate();
else activateBrowserWallet();
};
return (
<Box sx={styles.box}>
<Grid
container
direction='column'
alignItems='center'
justifyContent='center'
style={styles.vh100}
>
<Box position='absolute' top={8} right={16}>
<Button variant='contained' onClick={handleWalletConnection}>
{account
? `Disconnect ${account.substring(0, 5)}...`
: 'Connect Wallet'}
</Button>
</Box>
<Card sx={styles.card}>
<h1 style={styles.alignCenter}>Mint Your Token!</h1>
<SupplyComponent contract={contract} />
</Card>
</Grid>
</Box>
);
}
export default App;
我们的前端现在显示正确数据!
挑战
有一些附加信息可能有助于显示,例如连接账户当前拥有的Token数量:balanceOf(address)
。您可以自己将其添加到前端吗?
传送交易¶
现在是所有DApp中最重要的部分:状态更改交易。这是资金流动、Token铸造和价值传递的地方。
如果您回想我们的智能合约,我们希望通过调用purchaseMint
函数使用一些原生货币铸造一些Token。所以我们需要:
- 让用户指定输入多少数值的文本输入
- 让用户发起交易签名的按钮
让我们在名为MintingComponent.js
的新文件中创建一个名为MintingComponent
的新组件。首先,我们将处理文本输入,这将要求我们添加逻辑来存储要铸造的Token数量和文本字段元素。
import { useState } from 'react';
import { useContractFunction, useEthers, MoonbaseAlpha } from '@usedapp/core';
import { Button, CircularProgress, TextField, Grid } from '@mui/material';
import { utils } from 'ethers';
export default function MintingComponent({ contract }) {
const [value, setValue] = useState(0);
const textFieldStyle = { marginBottom: '16px' };
return (
<>
<Grid item xs={12}>
<TextField
type='number'
onChange={(e) => setValue(e.target.value)}
label='Enter value in DEV'
variant='outlined'
fullWidth
style={textFieldStyle}
/>
</Grid>
{/* This is where we'll add the button */}
</>
);
}
接着,我们需要创建发送交易的按钮,该按钮将调用合约的purchaseMint
。与合约交互会有点困难,因为您可能不太熟悉它。我们已经在前面的部分中完成了很多设置,因此实际上不需要太多代码:
export default function MintingComponent({ contract }) {
// ...
// Mint transaction
const { account, chainId, switchNetwork } = useEthers();
const { state, send } = useContractFunction(contract, 'purchaseMint');
const handlePurchaseMint = async () => {
if (chainId !== MoonbaseAlpha.chainId) {
await switchNetwork(MoonbaseAlpha.chainId);
}
send({ value: utils.parseEther(value.toString()) });
};
const isMining = state?.status === 'Mining';
return (
<>
{/* ... */}
<Grid item xs={12}>
<Button
variant='contained' color='primary' fullWidth
onClick={handlePurchaseMint}
disabled={state.status === 'Mining' || account == null}
>
{isMining? <CircularProgress size={24} /> : 'Purchase Mint'}
</Button>
</Grid>
</>
);
}
MintingComponent.js文档
import { useState } from 'react';
import { useContractFunction, useEthers, MoonbaseAlpha } from '@usedapp/core';
import { Button, CircularProgress, TextField, Grid } from '@mui/material';
import { utils } from 'ethers';
export default function MintingComponent({ contract }) {
const [value, setValue] = useState(0);
const textFieldStyle = { marginBottom: '16px' };
const { account, chainId, switchNetwork } = useEthers();
const { state, send } = useContractFunction(contract, 'purchaseMint');
const handlePurchaseMint = async () => {
if (chainId !== MoonbaseAlpha.chainId) {
await switchNetwork(MoonbaseAlpha.chainId);
}
send({ value: utils.parseEther(value.toString()) });
};
const isMining = state?.status === 'Mining';
return (
<>
<Grid item xs={12}>
<TextField
type='number'
onChange={(e) => setValue(e.target.value)}
label='Enter value in DEV'
variant='outlined'
fullWidth
style={textFieldStyle}
/>
</Grid>
<Grid item xs={12}>
<Button
variant='contained' color='primary' fullWidth
onClick={handlePurchaseMint}
disabled={state.status === 'Mining' || account == null}
>
{isMining? <CircularProgress size={24} /> : 'Purchase Mint'}
</Button>
</Grid>
</>
);
}
让我们来解析这些non-JSX代码:
- 用户的账户信息是通过
useEthers
检索的,这是可以完成的,因为useDApp在整个项目中提供了此信息 - useDApp中的
useContractFunction
hook用于创建一个函数send
,它将签署并发送一个交易,该交易调用由contract
对象定义的合约上的purchaseMint
函数 - 另一个函数
handlePurchaseMint
被定义来帮助将TextField
组件定义的原生Gas值注入到send
函数中。它首先检查用户的钱包是否连接到Moonbase Alpha,如果没有,它会提示用户切换网络 - 辅助常数将确定交易是否仍处于
Mining
阶段,即尚未完成
现在让我们看看视觉组件。该按钮将在按下时调用handlePurchaseMint
,这是有道理的。当交易发生时,如果用户未使用钱包连接到DApp(未定义账户价值时),该按钮也将被禁用。
这段代码本质上可以归结为将useContractFunction
hook与contract
对象结合使用,这比它在底层的作用要简单得多!让我们将此组件添加到主App.js
文件中。
// ... other imports
import MintingComponent from './MintingComponent';
function App() {
// ...
return (
{/* Wrapper Components */}
{/* Button Component */}
<Card sx={styles.card}>
<h1 style={styles.alignCenter}>Mint Your Token!</h1>
<SupplyComponent contract={contract} />
<MintingComponent contract={contract} />
</Card>
{/* Wrapper Components */}
)
}
App.js文档
import { useEthers } from '@usedapp/core';
import { Button, Grid, Card } from '@mui/material';
import { Box } from '@mui/system';
import { Contract } from 'ethers';
import MintableERC20 from './MintableERC20.json';
import SupplyComponent from './SupplyComponent';
import MintingComponent from './MintingComponent';
const styles = {
box: { minHeight: '100vh', backgroundColor: '#1b3864' },
vh100: { minHeight: '100vh' },
card: { borderRadius: 4, padding: 4, maxWidth: '550px', width: '100%' },
alignCenter: { textAlign: 'center' },
};
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
function App() {
const { activateBrowserWallet, deactivate, account } = useEthers();
const contract = new Contract(contractAddress, MintableERC20.abi);
// Handle the wallet toggle
const handleWalletConnection = () => {
if (account) deactivate();
else activateBrowserWallet();
};
return (
<Box sx={styles.box}>
<Grid
container
direction='column'
alignItems='center'
justifyContent='center'
style={styles.vh100}
>
<Box position='absolute' top={8} right={16}>
<Button variant='contained' onClick={handleWalletConnection}>
{account
? `Disconnect ${account.substring(0, 5)}...`
: 'Connect Wallet'}
</Button>
</Box>
<Card sx={styles.card}>
<h1 style={styles.alignCenter}>Mint Your Token!</h1>
<SupplyComponent contract={contract} />
<MintingComponent contract={contract} />
</Card>
</Grid>
</Box>
);
}
export default App;
如果您尝试输入0.1的数值并按下按钮,则会出现MetaMask提示。
合约中的读取事件¶
了解交易中发生的情况的一种常见方法是通过事件(也称为日志)。这些日志由智能合约通过emit
和event
关键字发出,在响应式前端中非常重要。通常DApp会使用toast元素来实时表示事件,但对于这个DApp,我们将使用一个简单的表格。
我们在智能合约中创建了一个事件: event PurchaseOccurred(address minter, uint256 amount)
,所以让我们弄清楚如何在前端显示其信息。
让我们在新文件PurchaseOccurredEvents.js
中创建一个新组件PurchaseOccurredEvents
,用于读取最后五个日志并将其显示在表格中:
import { useLogs, useBlockNumber } from '@usedapp/core';
import { utils } from 'ethers';
import {
Grid,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from '@mui/material';
export default function PurchaseOccurredEvents({ contract }) {
return (
<Grid item xs={12} marginTop={5}>
<TableContainer >
<Table>
<TableHead>
<TableRow>
<TableCell>Minter</TableCell>
<TableCell align='right'>Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{/* This is where we have to inject data from our logs! */}
</TableBody>
</Table>
</TableContainer>
</Grid>
);
}
此组件目前用于创建一个空表格,让我们使用两个新的hook来读取这些日志:
export default function PurchaseOccurredEvents({ contract }) {
// Get block number to ensure that the useLogs doesn't search from 0, otherwise it will time out
const blockNumber = useBlockNumber();
// Create a filter & get the logs
const filter = { args: [null, null], contract, event: 'PurchaseOccurred' };
const logs = useLogs(filter, { fromBlock: blockNumber - 10000 });
const parsedLogs = logs?.value.slice(-5).map(log => log.data);
// ...
}
以下为代码中发生的事情:
- 区块编码是从
useBlockNumber
hook接收的,类似于使用JSON-RPC函数eth_blockNumber
- 创建一个过滤器来过滤所有事件,其中合约上的任何参数都被注入到事件名称为
PurchaseOccurred
的组件中 - 通过
useLogs
hook查询日志,类似于使用eth_getLogs
JSON-RPC函数。请注意,我们只查询最后10,000个区块,否则将查询区块链的整个历史记录,并且RPC将超时 - 解析生成的日志,并选择最近的五个
如果我们希望展现他们,我们可以进行以下操作:
export default function PurchaseOccurredEvents({ contract }) {
// ...
return (
<Grid item xs={12} marginTop={5}>
<TableContainer >
<Table>
{/* TableHead Component */}
<TableBody>
{parsedLogs?.reverse().map((log, index) => (
<TableRow key={index}>
<TableCell>{log.minter}</TableCell>
<TableCell align='right'>
{utils.formatEther(log.amount)} tokens
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
);
}
PurchaseOccurredEvents.js文档
import { useLogs, useBlockNumber } from '@usedapp/core';
import { utils } from 'ethers';
import {
Grid,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from '@mui/material';
export default function PurchaseOccurredEvents({ contract }) {
// Get block number to ensure that the useLogs doesn't search from 0, otherwise it will time out
const blockNumber = useBlockNumber();
// Create a filter & get the logs
const filter = { args: [null, null], contract, event: 'PurchaseOccurred' };
const logs = useLogs(filter, { fromBlock: blockNumber - 10000 });
const parsedLogs = logs?.value.slice(-5).map((log) => log.data);
return (
<Grid item xs={12} marginTop={5}>
<TableContainer>
<Table>
<TableHead>
<TableRow>
<TableCell>Minter</TableCell>
<TableCell align='right'>Amount</TableCell>
</TableRow>
</TableHead>
<TableBody>
{parsedLogs?.reverse().map((log, index) => (
<TableRow key={index}>
<TableCell>{log.minter}</TableCell>
<TableCell align='right'>
{utils.formatEther(log.amount)} tokens
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
);
}
这同样会被添加至App.js
。
// ... other imports
import PurchaseOccurredEvents from './PurchaseOccurredEvents';
function App() {
// ...
return (
{/* Wrapper Components */}
{/* Button Component */}
<Card sx={styles.card}>
<h1 style={styles.alignCenter}>Mint Your Token!</h1>
<SupplyComponent contract={contract} />
<MintingComponent contract={contract} />
<PurchaseOccurredEvents contract={contract} />
</Card>
{/* Wrapper Components */}
)
}
App.js文档
import { useEthers } from '@usedapp/core';
import { Button, Grid, Card } from '@mui/material';
import { Box } from '@mui/system';
import { Contract } from 'ethers';
import MintableERC20 from './MintableERC20.json';
import SupplyComponent from './SupplyComponent';
import MintingComponent from './MintingComponent';
import PurchaseOccurredEvents from './PurchaseOccurredEvents';
const styles = {
box: { minHeight: '100vh', backgroundColor: '#1b3864' },
vh100: { minHeight: '100vh' },
card: { borderRadius: 4, padding: 4, maxWidth: '550px', width: '100%' },
alignCenter: { textAlign: 'center' },
};
const contractAddress = 'INSERT_CONTRACT_ADDRESS';
function App() {
const { activateBrowserWallet, deactivate, account } = useEthers();
const contract = new Contract(contractAddress, MintableERC20.abi);
// Handle the wallet toggle
const handleWalletConnection = () => {
if (account) deactivate();
else activateBrowserWallet();
};
return (
<Box sx={styles.box}>
<Grid
container
direction='column'
alignItems='center'
justifyContent='center'
style={styles.vh100}
>
<Box position='absolute' top={8} right={16}>
<Button variant='contained' onClick={handleWalletConnection}>
{account
? `Disconnect ${account.substring(0, 5)}...`
: 'Connect Wallet'}
</Button>
</Box>
<Card sx={styles.card}>
<h1 style={styles.alignCenter}>Mint Your Token!</h1>
<SupplyComponent contract={contract} />
<MintingComponent contract={contract} />
<PurchaseOccurredEvents contract={contract} />
</Card>
</Grid>
</Box>
);
}
export default App;
同时,如果您已完成了任何交易,您将会看见他们的弹窗!
现在您已经实现了DApp前端的三个主要组件:从存储中读取、发送交易和读取日志。有了这些构建模块以及您通过智能合约和节点获得的知识,您应该能够覆盖80%的DApp。
您可以在GitHub上查看完整的范例DApp。
结论¶
在本教程中,我们涵盖了完成DApp开发所必需的广泛主题和工具。我们从Hardhat开始,这是一个强大的开发环境,可以简化编写、测试和部署智能合约的过程。Ethers.js是一个与以太坊节点交互的流行库,被引入来管理钱包和交易。
我们深入研究了智能合约的编写过程,重点介绍了开发链上逻辑时的最佳实践和关键考虑因素。此教程随后探讨了useDApp,这是一个基于React的框架,用于创建用户友善的前端。我们讨论了从合约读取数据、执行交易和处理日志的技术,以确保无缝的用户体验。
当然,有更多进阶但不一定必要的DApp组件在教程中出现:
如果您有兴趣深入了解从Web2到Web3的内容,您可以阅读关于Web2与Web3开发的不同之处的博客文章。
希望通过阅读本指南,您能够顺利在Moonbeam上创建新颖的DApp!
本教程仅用于教育目的。 因此,不应在生产环境中使用本教程中创建的任何合约或代码。