项目概览
项目名称: 奶油社区 (Creamunity)
技术栈:
核心流程:
发行代币 (CreamCoin): 创建一个标准的 ERC-20 代币。
创建社交合约 (Creamunity): 负责处理发帖、打赏逻辑。
开发前端页面: 用户进行交互的界面。
实现核心功能: 创建钱包、登录、查余额、发帖 (消耗币并上链)、看帖、打赏。
Gas 费用代付: 后端服务器为用户的操作代付 gas 费用。
准备工作与环境搭建
1. 初始化项目
打开终端 PowerShell 创建项目。
1 2 3 4 5 6 7 8 9 10 11 12
| # 1. 创建一个项目文件夹 mkdir creamunity cd creamunity
# 2. 初始化 Node.js 项目 npm init -y
# 3. 安装 Hardhat - 专业的以太坊开发环境 npm install --save-dev hardhat
# 4. 启动 Hardhat npx hardhat
|
启动 Hardhat 时,它会询问:
选择 Create a JavaScript project
。
Hardhat project root
: 直接回车。
Do you want to add a .gitignore?
: y
(yes)。
Do you want to install sample project's dependencies with npm?
: y
(yes)。
阶段一:发行代币 (Token)
代币是项的经济基础,创建一个标准的 ERC-20 代币。为了安全和简单,使用 OpenZeppelin 提供的标准合约模板。
1.安装 OpenZeppelin 合约库
1
| npm install @openzeppelin/contracts
|
2.创建 CreamCoin.sol 合约
在 contracts/
文件夹下,创建一个新文件 CreamCoin.sol
,并写入以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; import "@openzeppelin/contracts/access/Ownable.sol"; // 引入 Ownable
// 标准的 ERC-20 代币合约 // 继承 OpenZeppelin 的 ERC20 和 Ownable 合约 contract CreamCoin is ERC20, Ownable {
// 构造函数,在部署时被调用一次 // initialOwner 是部署这个合约的钱包地址 constructor(address initialOwner) ERC20("Cream Coin", "CREAM") Ownable(initialOwner) { // 在这里可以给自己预先铸造一些币 // _mint(接收者地址, 数量) // 1000000 * 10^18 (ERC20 默认 18 位小数) _mint(msg.sender, 1000000 * 10**decimals()); }
// 提供一个公开的 mint 函数,只有合约拥有者(Owner)可以调用 function mint(address to, uint256 amount) public onlyOwner { _mint(to, amount); } }
|
代码解释:
ERC20("Cream Coin", "CREAM")
: 定义了代币的全名和简称。
Ownable(initialOwner)
: 设置部署者为合约的”拥有者”,只有拥有者能调用 onlyOwner
修饰的函数。
_mint(msg.sender, ...)
: 在合约部署时,给自己的钱包里铸造 1,000,000 个奶油币。
mint(...)
函数: 允许以后继续增发,比如给 faucet 或者新用户。
3.配置部署环境
需要告诉 Hardhat 如何连接到 Sepolia 测试网。
获取 RPC URL: Infura,创建一个新的 App,选择 Ethereum Sepolia 网络,然后复制其 HTTPS RPC URL。
获取私钥: 从 MetaMask 导出私钥。点击账户详情 -> 导出私钥。
安装 dotenv
: 为了安全地管理私钥和 URL,使用 .env
文件。
创建 .env
文件**: 在项目根目录创建 .env
文件,并写入:
1 2
| SEPOLIA_RPC_URL="_ALCHEMY_或_INFURA_的_URL" PRIVATE_KEY="_METAMASK_私钥"
|
修改 hardhat.config.js
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| require("@nomicfoundation/hardhat-toolbox"); require("dotenv").config();
module.exports = { solidity: "0.8.20", networks: { sepolia: { url: process.env.SEPOLIA_RPC_URL || "", accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], }, }, };
|
4. 编写部署脚本
在 scripts/ 文件夹下,创建一个新文件 deployCreamCoin.js。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const hre = require("hardhat");
async function main() { const [deployer] = await hre.ethers.getSigners(); console.log("Deploying contracts with the account:", deployer.address);
const creamCoin = await hre.ethers.deployContract("CreamCoin", [deployer.address]);
await creamCoin.waitForDeployment();
console.log( `CreamCoin deployed to: ${creamCoin.target}` ); }
main().catch((error) => { console.error(error); process.exitCode = 1; });
|
5. 部署!
运行以下命令,将代币部署到 Sepolia 测试网。
1
| npx hardhat run scripts/deployCreamCoin.js --network sepolia
|
成功后,终端会输出 CreamCoin deployed to: 0x...
。**这个 0x...
地址就是的奶油币合约地址,请务必保存好!可以在 Sepolia Etherscan 上搜索合约地址,查看它的信息。
阶段二:创建社交合约 (Creamunity)
这个合约是项目的核心,处理留言和打赏。
1. 创建 Creamunity.sol 合约
在 contracts/
文件夹下创建 Creamunity.sol
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; // 引入 ERC20 接口
contract Creamunity { // --- 状态变量 ---
IERC20 public creamCoin; // 奶油币合约的引用 address public burnAddress = 0x000000000000000000000000000000000000dEaD; // 黑洞地址 uint256 public postFee = 10 * 10**18; // 每次发言消耗10个币 (假设18位小数) uint256 public tipAmount = 1 * 10**18; // 每次打赏固定1个币
// --- 留言结构体 --- struct Message { uint256 id; string content; // 留言内容 address author; // 作者地址 uint256 timestamp; // 发布时间 uint256 totalTips; // 收到的总打赏 }
// 用一个数组存储所有留言 Message[] public messages; // 用于快速通过 id 查找留言在数组中的索引 mapping(uint256 => uint256) private messageIndexById;
// --- 事件 --- // 方便前端监听新消息和打赏 event NewMessage(uint256 id, address indexed author, string content, uint256 timestamp); event NewTip(uint256 indexed messageId, address indexed tipper, address indexed author);
// --- 构造函数 --- constructor(address _creamCoinAddress) { creamCoin = IERC20(_creamCoinAddress); }
// --- 核心功能:发布留言 --- function postMessage(string memory _content) public { // 1. 检查用户是否有足够的奶油币并已授权 // transferFrom 会检查授权额度,如果不足或未授权,会自动失败 // 2. 从用户钱包转 10 个币到黑洞地址 (销毁) bool sent = creamCoin.transferFrom(msg.sender, burnAddress, postFee); require(sent, "Token transfer failed");
// 3. 创建新留言 uint256 messageId = messages.length; messages.push(Message({ id: messageId, content: _content, author: msg.sender, timestamp: block.timestamp, totalTips: 0 })); messageIndexById[messageId] = messageId; // 在这个简单模型里,id和index相同
// 4. 触发新留言事件 emit NewMessage(messageId, msg.sender, _content, block.timestamp); }
// --- 核心功能:打赏留言 --- function tipMessage(uint256 _messageId) public { // 确保留言存在 require(_messageId < messages.length, "Message does not exist");
Message storage messageToTip = messages[messageIndexById[_messageId]];
// 打赏者不能是作者本人 require(msg.sender != messageToTip.author, "Cannot tip your own message");
// 1. 从打赏者钱包转 1 个币给留言作者 bool sent = creamCoin.transferFrom(msg.sender, messageToTip.author, tipAmount); require(sent, "Token transfer failed");
// 2. 更新留言的总打赏额 messageToTip.totalTips += tipAmount;
// 3. 触发打赏事件 emit NewTip(_messageId, msg.sender, messageToTip.author); }
// --- 读取功能 --- function getAllMessages() public view returns (Message[] memory) { return messages; }
function getMessageCount() public view returns (uint256) { return messages.length; } }
|
2. 部署 Creamunity 合约
创建部署脚本 scripts/deployCreamunity.js。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const hre = require("hardhat");
async function main() { const [deployer] = await hre.ethers.getSigners(); console.log("Deploying contracts with the account:", deployer.address);
const creamCoinAddress = "_CREAMCOIN_合约地址";
const creamunity = await hre.ethers.deployContract("Creamunity", [creamCoinAddress]);
await creamunity.waitForDeployment();
console.log( `Creamunity deployed to: ${creamunity.target}` ); }
main().catch((error) => { console.error(error); process.exitCode = 1; });
|
然后运行部署命令:
1
| npx hardhat run scripts/deployCreamunity.js --network sepolia
|
保存好输出的 Creamunity
合约地址。现在两个核心合约都已上线。
阶段三:开发前端界面
前端仓库已部署到远程仓库
阶段四:GAS 费用代付
这个模型将一次用户操作拆分成了三个步骤:
前端:
后端(服务器):
前端:
创建后端自动转账脚本
使用 Node.js 和 Express 创建一个简单的后台服务,它只有一个功能:接收前端请求并给用户转账 ETH。
在项目根目录 (creamunity
) 下创建新文件夹并初始化
1 2 3 4
| mkdir backend cd backend npm init -y npm install express ethers dotenv cors winston
|
express
: 用于创建 web 服务。
ethers
: 用于和以太坊交互。
dotenv
: 用于安全管理开发者私钥。
cors
: 解决跨域请求问题。
winston
: 输出日志
在 backend
文件夹中创建 .env
文件 这个文件用来存放开发者钱包私钥
1 2 3 4 5
| # 开发者钱包私钥 DEVELOPER_PRIVATE_KEY="开发者钱包私钥"
# Sepolia RPC URL SEPOLIA_RPC_URL="SEPOLIA_RPC_URL"
|
在 backend
文件夹中创建 server.js
文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| const express = require('express'); const ethers = require('ethers'); const cors = require('cors'); require('dotenv').config();
const app = express(); app.use(cors()); app.use(express.json());
const PORT = 3000;
const provider = new ethers.providers.JsonRpcProvider(process.env.SEPOLIA_RPC_URL); const developerWallet = new ethers.Wallet(process.env.DEVELOPER_PRIVATE_KEY, provider);
console.log(`自动化Gas支援服务启动...`); console.log(`开发者钱包地址: ${developerWallet.address}`);
app.post('/request-gas', async (req, res) => { const { userAddress, amount } = req.body;
if (!ethers.utils.isAddress(userAddress) || !amount) { return res.status(400).json({ message: '无效的地址或金额' }); }
console.log(`收到请求: 向 ${userAddress} 转账 ${ethers.utils.formatEther(amount)} ETH`);
try { const tx = { to: userAddress, value: ethers.BigNumber.from(amount), };
const txResponse = await developerWallet.sendTransaction(tx); console.log(`交易已发送,Hash: ${txResponse.hash}`);
res.status(200).json({ message: '转账请求已发送', txHash: txResponse.hash, });
} catch (error) { console.error('转账失败:', error); res.status(500).json({ message: '服务器内部错误,转账失败' }); } });
app.listen(PORT, () => { console.log(`服务器正在监听端口 ${PORT}`); });
|
创建日志配置文件 (logger.js
)
为了让代码更整洁,把所有日志相关的配置放进一个单独的文件里。
在 backend
文件夹中,创建一个新的JavaScript文件,命名为 logger.js
。
将以下代码粘贴到 logger.js
文件中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
const winston = require('winston');
const logFormat = winston.format.printf(({ level, message, timestamp }) => { return `${timestamp} [${level.toUpperCase()}]: ${message}`; });
const logger = winston.createLogger({ format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat ),
transports: [ new winston.transports.File({ filename: 'server.log', level: 'info' }),
new winston.transports.Console({ format: winston.format.combine( winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat ), level: 'info' }) ],
exceptionHandlers: [ new winston.transports.File({ filename: 'exceptions.log' }) ] });
module.exports = logger;
|
现在会看到:
控制台输出:终端里的所有日志,现在每一行前面都会有 2025-06-13 15:52:00 [INFO]:
这样的格式,并且带有颜色。
新的日志文件:在 backend
文件夹下,会自动创建一个名为 server.log
的文件。
文件内容:打开 server.log
,会发现里面记录了和控制台完全一样的、带有时间戳的日志信息。
阶段五:上线后端服务
安装 Nginx ,用来作为反向代理。
1
| sudo apt install nginx -y
|
安装 PM2 (进程管理器)
如果直接用 node server.js
运行的后端,当关闭SSH连接时,程序就会中断。PM2是一个进程管理器,它能让的Node.js应用在后台稳定运行,并在服务器重启后自动启动。
现在需要把本地的 creamunity
文件夹整个上传到VPS上。
将本地的 creamunity
项目推送到一个私有的GitHub或GitLab仓库。
在VPS上安装Git:sudo apt install git -y
。
在VPS上找一个合适的位置(如 /var/www/
)克隆项目:
1 2 3 4 5 6
| sudo mkdir -p /var/www sudo chown -R $(whoami) /var/www cd /var/www
git clone GIT_REPOSITORY_URL
|
以后更新代码,只需要在服务器上运行 git pull
即可。
代码上传后,需要配置并启动后端。.env
文件因为包含密钥,通常不会上传到Git。需要在服务器上手动创建它。
1 2 3
| cd /var/www/creamunity/backend npm install nano .env
|
在打开的编辑器中,把你本地 .env
文件里的所有内容(DEVELOPER_PRIVATE_KEY
, ETHERSCAN_API_KEY
等)复制粘贴进去。按 Ctrl+X
,然后按 Y
,再按回车来保存并退出。
用 PM2 启动后端服务:
1
| pm2 start server.js --name "cream-backend"
|
--name
参数为应用起了一个名字,方便管理。
配置 Nginx (反向代理)
这是最关键的一步,要告诉Nginx:
创建 Nginx 配置文件:
1
| sudo nano /etc/nginx/sites-available/creamunity
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| server { listen 80; listen [::]:80;
server_name api.domain.com;
location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_cache_bypass $http_upgrade; } }
|
最后启用配置
1 2 3
| sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/creamunity/etc/nginx/sites-enabled/
|
测试并重启 Nginx
1 2
| sudo nginx -t sudo systemctl restart nginx
|
现在服务已经可以通过域名访问,请务必为API启用HTTPS加密。在VPS上运行 certbot
(在之前的步骤中已经安装过):
1 2
| sudo apt install certbot python3-certbot-nginx -y sudo certbot --nginx -d api.your-domain.com
|
Certbot会引导完成几个简单的步骤,然后它会自动获取SSL证书,并自动修改Nginx配置以启用HTTPS。完成后,所有到 http://api.your-domain.com
的请求都会被自动重定向到 https://
。
至此,一个全栈DApp社区项目从本地部署到了线上生产环境