dApp开发入门教程:从零开始构建链上留言板

1.什么是dApp?dApp(去中心化应用)是基于区块链技术构建的应用程序,与传统应用的主要区别在于:去中心化:数据存储在区块链上,不依赖单一服务器透明公开:智能合约代码和数据对所有人可见用户控制:用户完全控制自己的数据和资产无需信任:通过智能合约自动执行,无需第三方中介本教程将指

1. 什么是dApp?

dApp(去中心化应用)是基于区块链技术构建的应用程序,与传统应用的主要区别在于:

  • 去中心化:数据存储在区块链上,不依赖单一服务器
  • 透明公开:智能合约代码和数据对所有人可见
  • 用户控制:用户完全控制自己的数据和资产
  • 无需信任:通过智能合约自动执行,无需第三方中介

本教程将指导你从零开始构建一个完整的dApp——链上留言板,让你掌握dApp开发的核心流程。

2. 开发环境搭建

2.1 安装Node.js

dApp开发需要Node.js环境,推荐使用LTS版本:

  1. 访问 Node.js官网
  2. 下载并安装适合你操作系统的LTS版本
  3. 验证安装:
    node -v
    npm -v

2.2 安装MetaMask

MetaMask是连接dApp和区块链的桥梁:

  1. 访问 MetaMask官网
  2. 下载并安装浏览器插件
  3. 创建钱包并保存助记词
  4. 添加Trustivon测试网络(参考MetaMask网络设置

2.3 获取测试代币

在Trustivon测试网上开发需要测试代币:

  1. 访问 Trustivon水龙头
  2. 输入你的MetaMask地址
  3. 领取测试代币

3. 智能合约开发

3.1 初始化Hardhat项目

Hardhat是以太坊智能合约开发的流行框架:

  1. 创建项目目录:

    mkdir doomsday-dapp
    cd doomsday-dapp
  2. 初始化npm项目:

    npm init -y
  3. 安装Hardhat:

    npm install --save-dev hardhat
  4. 初始化Hardhat项目:

    npx hardhat init
    • 选择"Create a TypeScript project"
    • 按默认选项完成初始化
  5. 安装依赖:

    npm install --save-dev @nomicfoundation/hardhat-toolbox
    npm install dotenv

3.2 编写智能合约

创建一个简单的链上留言板合约:

  1. 创建合约文件:

    mkdir -p contracts
    touch contracts/MessageBoard.sol
  2. 编写合约代码:

    // 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 {}
    }

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网络配置
  4. 部署合约到Trustivon测试网:

    npx hardhat run scripts/deploy.js --network trustivon

4. 前端开发

4.1 初始化React项目

  1. 创建前端目录:

    npx create-react-app frontend
    cd frontend
    npm install web3 @web3-react/core @web3-react/injected-connector
  2. 创建合约ABI目录:

    mkdir -p src/contracts
  3. 复制合约ABI:

    cp ../artifacts/contracts/MessageBoard.sol/MessageBoard.json src/contracts/

4.2 编写前端代码

  1. 创建Web3连接组件:

    mkdir -p src/components
    touch src/components/Web3Provider.js
  2. 编写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>;
    };
  3. 创建留言板组件:

    touch src/components/MessageBoard.js
  4. 编写留言板代码:

    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;
  5. 更新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 运行前端应用

  1. 启动前端开发服务器:

    npm start
  2. 在浏览器中访问 http://localhost:3000

  3. 连接MetaMask钱包

  4. 测试留言功能:

    • 输入留言内容
    • 设置竞价金额
    • 提交留言
    • 查看留言列表

5. 部署和上线

5.1 构建前端应用

  1. 构建生产版本:

    npm run build
  2. 部署到静态网站托管服务(如Vercel、Netlify等)

5.2 验证和测试

  1. 在浏览器中访问部署后的网站
  2. 测试所有功能
  3. 确保与MetaMask正常交互
  4. 检查交易是否正确上链

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

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
trustivon.com
trustivon.com
0x6BC0...2776
Provide one-stop compatible Ethereum high-performance public chain solutions