1.什么是dApp?dApp(去中心化应用)是基于区块链技术构建的应用程序,与传统应用的主要区别在于:去中心化:数据存储在区块链上,不依赖单一服务器透明公开:智能合约代码和数据对所有人可见用户控制:用户完全控制自己的数据和资产无需信任:通过智能合约自动执行,无需第三方中介本教程将指
dApp(去中心化应用)是基于区块链技术构建的应用程序,与传统应用的主要区别在于:
本教程将指导你从零开始构建一个完整的dApp——链上留言板,让你掌握dApp开发的核心流程。
dApp开发需要Node.js环境,推荐使用LTS版本:
node -v
npm -vMetaMask是连接dApp和区块链的桥梁:
在Trustivon测试网上开发需要测试代币:
Hardhat是以太坊智能合约开发的流行框架:
创建项目目录:
mkdir doomsday-dapp
cd doomsday-dapp
初始化npm项目:
npm init -y
安装Hardhat:
npm install --save-dev hardhat
初始化Hardhat项目:
npx hardhat init
安装依赖:
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install dotenv
创建一个简单的链上留言板合约:
创建合约文件:
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名的最低竞价
* @return 最低竞价金额,如果留言数量不足100,则返回0
*/
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 {}
}
编译合约:
npx hardhat compile
编写测试文件:
mkdir -p test
touch test/MessageBoard.test.js
编写测试代码:
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);
});
});
运行测试:
npx hardhat test
创建部署脚本:
mkdir -p scripts
touch scripts/deploy.js
编写部署代码:
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);
});
配置Trustivon网络:
.env文件:
touch .envPRIVATE_KEY=your-private-key
TRUSTIVON_RPC_URL=https://rpc.trustivon.comhardhat.config.js,添加Trustivon网络配置部署合约到Trustivon测试网:
npx hardhat run scripts/deploy.js --network trustivon
创建前端目录:
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/
创建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;
启动前端开发服务器:
npm start
在浏览器中访问 http://localhost:3000
连接MetaMask钱包
测试留言功能:
构建生产版本:
npm run build
部署到静态网站托管服务(如Vercel、Netlify等)
通过本教程,你已经学会了:
你可以进一步扩展这个dApp:
恭喜你完成了第一个dApp的开发!继续学习和探索,你将能够构建更复杂和强大的去中心化应用。
线上预览:https://eternal.trustivon.com/ GitHub开源:https://github.com/Trustivon/eternal-message-dapp
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!