dApp开发入门教程:从零开始构建链上留言板
1. 什么是dApp?
dApp(去中心化应用)是基于区块链技术构建的应用程序,与传统应用的主要区别在于:
- 去中心化:数据存储在区块链上,不依赖单一服务器
- 透明公开:智能合约代码和数据对所有人可见
- 用户控制:用户完全控制自己的数据和资产
- 无需信任:通过智能合约自动执行,无需第三方中介
本教程将指导你从零开始构建一个完整的dApp——链上留言板,让你掌握dApp开发的核心流程。
2. 开发环境搭建
2.1 安装Node.js
dApp开发需要Node.js环境,推荐使用LTS版本:
- 访问 Node.js官网
- 下载并安装适合你操作系统的LTS版本
验证安装:
node -v npm -v
2.2 安装MetaMask
MetaMask是连接dApp和区块链的桥梁:
- 访问 MetaMask官网
- 下载并安装浏览器插件
- 创建钱包并保存助记词
- 添加Trustivon测试网络(参考MetaMask网络设置)
2.3 获取测试代币
在Trustivon测试网上开发需要测试代币:
- 访问 Trustivon水龙头
- 输入你的MetaMask地址
- 领取测试代币
3. 智能合约开发
3.1 初始化Hardhat项目
Hardhat是以太坊智能合约开发的流行框架:
创建项目目录:
mkdir doomsday-dapp cd doomsday-dapp初始化npm项目:
npm init -y安装Hardhat:
npm install --save-dev hardhat初始化Hardhat项目:
npx hardhat init- 选择"Create a TypeScript project"
- 按默认选项完成初始化
安装依赖:
npm install --save-dev @nomicfoundation/hardhat-toolbox npm install dotenv
3.2 编写智能合约
创建一个简单的链上留言板合约:
创建合约文件:
mkdir -p contracts touch contracts/MessageBoard.sol编写合约代码:
// SPDX-License-Identifier: MIT pragma solidity ^0.8.19; contract MessageBoard { struct Message { string content; address sender; uint256 bidAmount; uint256 timestamp; bytes32 messageId; } Message[] public messages; uint256 public constant MAX_MESSAGES = 100; uint256 public constant DECAY_INTERVAL = 24 hours; // 衰减间隔:24小时 uint256 public constant DECAY_RATE = 50; // 衰减率:50% uint256 public lastMessageTimestamp; // 最后一条留言的时间戳 event MessageAdded( bytes32 messageId, string content, address sender, uint256 bidAmount, uint256 timestamp ); /** * @dev 获取当前能进入前100名的最低竞价
*/
function getMinimumBidForTop100() public view returns (uint256) {
if (messages.length < MAX_MESSAGES) {
// 如果留言数量不足100,任何大于0的竞价都能进入前100
return 0;
}
// 获取第100条留言的原始竞价
uint256 originalBid = messages[MAX_MESSAGES - 1].bidAmount;
// 如果最后一条留言时间为0,说明还没有留言,返回0
if (lastMessageTimestamp == 0) {
return originalBid;
}
// 计算自最后一条留言以来经过的时间
uint256 timeElapsed = block.timestamp - lastMessageTimestamp;
// 如果经过的时间小于衰减间隔,返回原始竞价
if (timeElapsed < DECAY_INTERVAL) {
return originalBid;
}
// 计算经过了多少个衰减周期
uint256 decayPeriods = timeElapsed / DECAY_INTERVAL;
// 计算衰减后的竞价
uint256 decayedBid = originalBid;
for (uint256 i = 0; i < decayPeriods; i++) {
// 每次衰减50%
decayedBid = decayedBid * (100 - DECAY_RATE) / 100;
// 防止衰减到0以下
if (decayedBid == 0) {
break;
}
}
return decayedBid;
}
function addMessage(string calldata _content, bytes32 _messageId) external payable {
require(msg.value > 0, "Bid amount must be greater than 0");
// 检查当前竞价是否大于等于进入前100名的最低竞价
uint256 minimumBid = getMinimumBidForTop100();
require(msg.value > minimumBid, "Bid amount must be greater than the current minimum bid for top 100");
Message memory newMessage = Message({
content: _content,
sender: msg.sender,
bidAmount: msg.value,
timestamp: block.timestamp,
messageId: _messageId
});
// 插入排序,按竞价金额降序
uint256 i = messages.length;
messages.push(newMessage);
while (i > 0 && messages[i - 1].bidAmount < newMessage.bidAmount) {
messages[i] = messages[i - 1];
i--;
}
if (i != messages.length - 1) {
messages[i] = newMessage;
}
// 只保留前100条留言
if (messages.length > MAX_MESSAGES) {
messages.pop();
}
// 更新最后一条留言的时间戳
lastMessageTimestamp = block.timestamp;
emit MessageAdded(
_messageId,
_content,
msg.sender,
msg.value,
block.timestamp
);
}
function getMessages() external view returns (Message[] memory) {
return messages;
}
/**
* @dev 分页获取留言
* @param _page 页码,从1开始
* @param _pageSize 每页数量
* @return 分页后的留言数组
*/
function getMessagesPaginated(uint256 _page, uint256 _pageSize) external view returns (Message[] memory) {
require(_page > 0, "Page must be greater than 0");
require(_pageSize > 0, "Page size must be greater than 0");
uint256 totalMessages = messages.length;
uint256 startIndex = (_page - 1) * _pageSize;
// 如果起始索引大于等于总留言数,返回空数组
if (startIndex >= totalMessages) {
return new Message[](0);
}
// 计算结束索引
uint256 endIndex = startIndex + _pageSize;
if (endIndex > totalMessages) {
endIndex = totalMessages;
}
// 创建结果数组
uint256 resultSize = endIndex - startIndex;
Message[] memory result = new Message[](resultSize);
// 填充结果数组
for (uint256 i = 0; i < resultSize; i++) {
result[i] = messages[startIndex + i];
}
return result;
}
function getMessageCount() external view returns (uint256) {
return messages.length;
}
// 允许合约接收ETH
receive() external payable {}
// 允许合约接收ETH(当调用不存在的函数时)
fallback() external payable {}}
### 3.3 编译和测试合约
1. 编译合约:npx hardhat compile
2. 编写测试文件:mkdir -p test
touch test/MessageBoard.test.js
3. 编写测试代码:const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MessageBoard", function () {
let MessageBoard;
let messageBoard;
let owner;
let addr1;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
MessageBoard = await ethers.getContractFactory("MessageBoard");
messageBoard = await MessageBoard.deploy();
await messageBoard.deployed();
});
it("Should add a message", async function () {
const content = "Hello, Trustivon!";
const messageId = ethers.utils.formatBytes32String("test-1");
const bidAmount = ethers.utils.parseEther("0.1");
await expect(
messageBoard.addMessage(content, messageId, { value: bidAmount })
)
.to.emit(messageBoard, "MessageAdded")
.withArgs(messageId, content, owner.address, bidAmount, expect.any(BigInt));
const messages = await messageBoard.getMessages();
expect(messages.length).to.equal(1);
expect(messages[0].content).to.equal(content);
expect(messages[0].sender).to.equal(owner.address);
});});
4. 运行测试:npx hardhat test
### 3.4 部署合约
1. 创建部署脚本:mkdir -p scripts
touch scripts/deploy.js
2. 编写部署代码:const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
console.log("Account balance:", (await deployer.getBalance()).toString());
const MessageBoard = await ethers.getContractFactory("MessageBoard");
const messageBoard = await MessageBoard.deploy();
await messageBoard.deployed();
console.log("MessageBoard contract deployed to:", messageBoard.address);}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
3. 配置Trustivon网络:
- 创建`.env`文件: touch .env
```添加配置:
PRIVATE_KEY=your-private-key TRUSTIVON_RPC_URL=https://rpc.trustivon.com- 修改
hardhat.config.js,添加Trustivon网络配置
部署合约到Trustivon测试网:
npx hardhat run scripts/deploy.js --network trustivon
4. 前端开发
4.1 初始化React项目
创建前端目录:
npx create-react-app frontend cd frontend npm install web3 @web3-react/core @web3-react/injected-connector创建合约ABI目录:
mkdir -p src/contracts复制合约ABI:
cp ../artifacts/contracts/MessageBoard.sol/MessageBoard.json src/contracts/
4.2 编写前端代码
创建Web3连接组件:
mkdir -p src/components touch src/components/Web3Provider.js编写Web3连接代码:
import React, { createContext, useContext, useEffect, useState } from 'react'; import { InjectedConnector } from '@web3-react/injected-connector'; import Web3 from 'web3'; const Web3Context = createContext(); export const useWeb3 = () => useContext(Web3Context); export const Web3Provider = ({ children }) => { const [web3, setWeb3] = useState(null); const [account, setAccount] = useState(null); const [networkId, setNetworkId] = useState(null); const [loading, setLoading] = useState(true); const connector = new InjectedConnector({ supportedChainIds: [19478], // Trustivon测试网链ID }); const connectWallet = async () => { try { const accounts = await connector.activate(); setAccount(accounts[0]); } catch (error) { console.error('Failed to connect wallet:', error); } }; useEffect(() => { const initWeb3 = async () => { try { if (window.ethereum) { const web3Instance = new Web3(window.ethereum); setWeb3(web3Instance); const network = await web3Instance.eth.net.getId(); setNetworkId(network); const accounts = await web3Instance.eth.getAccounts(); if (accounts.length > 0) { setAccount(accounts[0]); } } } catch (error) { console.error('Failed to initialize Web3:', error); } finally { setLoading(false); } }; initWeb3(); // 监听账户变化 window.ethereum?.on('accountsChanged', (accounts) => { setAccount(accounts[0]); }); // 监听网络变化 window.ethereum?.on('chainChanged', (chainId) => { setNetworkId(parseInt(chainId, 16)); }); }, []); const value = { web3, account, networkId, loading, connectWallet, }; return <Web3Context.Provider value={value}>{children}</Web3Context.Provider>; };创建留言板组件:
touch src/components/MessageBoard.js编写留言板代码:
import React, { useState, useEffect } from 'react'; import { useWeb3 } from './Web3Provider'; import contractABI from '../contracts/MessageBoard.json'; const CONTRACT_ADDRESS = 'your-contract-address'; // 替换为你的合约地址 const MessageBoard = () => { const { web3, account, connectWallet } = useWeb3(); const [messages, setMessages] = useState([]); const [content, setContent] = useState(''); const [bidAmount, setBidAmount] = useState('0.1'); const [loading, setLoading] = useState(false); const contract = web3 && new web3.eth.Contract(contractABI.abi, CONTRACT_ADDRESS); const fetchMessages = async () => { if (!contract) return; try { const result = await contract.methods.getMessages().call(); setMessages(result); } catch (error) { console.error('Failed to fetch messages:', error); } }; useEffect(() => { fetchMessages(); }, [contract]); const addMessage = async (e) => { e.preventDefault(); if (!contract || !account) return; setLoading(true); try { const messageId = web3.utils.sha3(Date.now().toString()); const value = web3.utils.toWei(bidAmount, 'ether'); await contract.methods .addMessage(content, messageId) .send({ from: account, value }); setContent(''); setBidAmount('0.1'); fetchMessages(); } catch (error) { console.error('Failed to add message:', error); } finally { setLoading(false); } }; if (!account) { return ( <div className="flex justify-center items-center h-screen"> <button onClick={connectWallet} className="px-6 py-3 bg-blue-500 text-white rounded-lg hover:bg-blue-600" > Connect Wallet </button> </div> ); } return ( <div className="container mx-auto p-4"> <h1 className="text-3xl font-bold mb-6 text-center">链上留言板</h1> <form onSubmit={addMessage} className="mb-8"> <div className="mb-4"> <label className="block text-sm font-medium mb-2">留言内容</label> <textarea value={content} onChange={(e) => setContent(e.target.value)} className="w-full p-2 border border-gray-300 rounded" rows="3" required /> </div> <div className="mb-4"> <label className="block text-sm font-medium mb-2">竞价金额 (TC)</label> <input type="number" value={bidAmount} onChange={(e) => setBidAmount(e.target.value)} className="w-full p-2 border border-gray-300 rounded" step="0.1" min="0.1" required /> </div> <button type="submit" className="w-full py-2 bg-green-500 text-white rounded hover:bg-green-600" disabled={loading} > {loading ? '提交中...' : '提交留言'} </button> </form> <div className="space-y-4"> <h2 className="text-2xl font-bold mb-4">留言列表</h2> {messages.length === 0 ? ( <p className="text-center text-gray-500">暂无留言</p> ) : ( messages.map((msg, index) => ( <div key={index} className="border border-gray-300 rounded p-4"> <div className="flex justify-between items-center mb-2"> <span className="font-bold">{msg.sender}</span> <span className="text-sm text-gray-500"> {new Date(msg.timestamp * 1000).toLocaleString()} </span> </div> <p className="mb-2">{msg.content}</p> <div className="text-right text-sm text-blue-600"> 竞价: {web3.utils.fromWei(msg.bidAmount, 'ether')} TC </div> </div> )) )} </div> </div> ); }; export default MessageBoard;更新App.js:
import React from 'react'; import './App.css'; import { Web3Provider } from './components/Web3Provider'; import MessageBoard from './components/MessageBoard'; function App() { return ( <Web3Provider> <div className="App"> <MessageBoard /> </div> </Web3Provider> ); } export default App;
4.3 运行前端应用
启动前端开发服务器:
npm start- 在浏览器中访问
http://localhost:3000 - 连接MetaMask钱包
测试留言功能:
- 输入留言内容
- 设置竞价金额
- 提交留言
- 查看留言列表
5. 部署和上线
5.1 构建前端应用
构建生产版本:
npm run build- 部署到静态网站托管服务(如Vercel、Netlify等)
5.2 验证和测试
- 在浏览器中访问部署后的网站
- 测试所有功能
- 确保与MetaMask正常交互
- 检查交易是否正确上链
6. 总结和扩展
6.1 开发总结
通过本教程,你已经学会了:
- 搭建dApp开发环境
- 编写和部署智能合约
- 开发React前端应用
- 连接Web3和MetaMask
- 与智能合约交互
6.2 扩展建议
你可以进一步扩展这个dApp:
- 添加用户认证和个人中心
- 实现留言编辑和删除功能
- 添加留言点赞和评论功能
- 实现留言搜索和筛选
- 添加链上身份验证
- 优化前端UI/UX设计
6.3 学习资源
7. 常见问题
7.1 无法连接MetaMask
- 确保MetaMask已安装并解锁
- 确保已添加Trustivon测试网络
- 刷新页面后重试
7.2 交易失败
- 确保钱包中有足够的测试代币
- 检查Gas费用设置
- 查看MetaMask交易记录中的错误信息
7.3 留言不显示
- 检查合约地址是否正确
- 确保网络连接正常
- 刷新页面后重试
8. 社区支持
恭喜你完成了第一个dApp的开发!继续学习和探索,你将能够构建更复杂和强大的去中心化应用。
线上预览:https://eternal.trustivon.com/
GitHub开源:https://github.com/Trustivon/eternal-message-dapp
当前页面是本站的「Google AMP」版。查看和发表评论请点击:完整版 »