作为一名区块链全栈工程师,日常工作的核心在于打通“链上逻辑”与“链下交互”之间的鸿沟。本文从工程实践角度,拆解一个完整DApp的开发链路,并给出可运行的关键代码。
典型全栈组合为:Solidity + Hardhat(合约层)、ethers.js v6 + React + wagmi(前端层)、The Graph 或自建索引器(数据层)。这套组合兼顾开发效率与生产可用性。
合约设计需考虑权限控制、重入防护与事件索引。以下是一个精简的ERC-721铸造合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract MintNFT is ERC721URIStorage, Ownable, ReentrancyGuard {
uint256 public nextTokenId;
uint256 public mintPrice = 0.01 ether;
uint256 public maxSupply = 1000;
mapping(address => bool) public whitelist;
event Minted(address indexed to, uint256 indexed tokenId, string uri);
constructor() ERC721("FullStackNFT", "FSN") Ownable(msg.sender) {}
function setWhitelist(address[] calldata users, bool status) external onlyOwner {
for (uint256 i = 0; i < users.length; i++) {
whitelist[users[i]] = status;
}
}
function mint(string calldata uri) external payable nonReentrant {
require(nextTokenId < maxSupply, "Sold out");
require(msg.value >= mintPrice, "Insufficient fee");
if (!whitelist[msg.sender]) {
require(msg.value >= mintPrice * 2, "Public price higher");
}
uint256 tokenId = nextTokenId++;
_safeMint(msg.sender, tokenId);
_setTokenURI(tokenId, uri);
emit Minted(msg.sender, tokenId, uri);
}
function withdraw() external onlyOwner {
(bool ok, ) = payable(owner()).call{value: address(this).balance}("");
require(ok, "Withdraw failed");
}
}工程要点:使用OpenZeppelin成熟组件降低安全风险;nonReentrant防护铸造类攻击;事件携带uri便于链下索引;白名单与公开价格分层是常见商业逻辑。
前端需处理钱包连接、网络切换、交易签名与状态回显。以下是一个可复用的合约Hook:
import { BrowserProvider, Contract, parseEther } from "ethers";
import { useState, useCallback } from "react";
const ABI = [
"function mint(string uri) payable",
"function nextTokenId() view returns (uint256)",
"event Minted(address indexed to, uint256 indexed tokenId, string uri)"
];
const CONTRACT_ADDRESS = "0xYourContractAddress";
export function useMint() {
const [loading, setLoading] = useState(false);
const [txHash, setTxHash] = useState<string>();
const mint = useCallback(async (uri: string) => {
if (!window.ethereum) throw new Error("请安装MetaMask");
setLoading(true);
try {
const provider = new BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const contract = new Contract(CONTRACT_ADDRESS, ABI, signer);
const price = parseEther("0.02");
const tx = await contract.mint(uri, { value: price });
setTxHash(tx.hash);
const receipt = await tx.wait(1);
// 解析事件,获取tokenId
const log = receipt.logs
.map((l: any) => { try { return contract.interface.parseLog(l); } catch { return null; } })
.find((e: any) => e?.name === "Minted");
return { tokenId: log?.args?.tokenId?.toString(), hash: tx.hash };
} finally {
setLoading(false);
}
}, []);
return { mint, loading, txHash };
}工程要点:v6中BrowserProvider替代了旧版Web3Provider;tx.wait(1)确保确认数;事件解析应做try/catch容错,因为交易日志中可能混入其他合约事件。
链上不适合存储大对象,token URI通常指向IPFS或中心化CDN。工程上需注意:
eth_call。nonce与pending状态,防止用户重复点击导致nonce冲突。Minted事件应被The Graph或自建服务消费,前端列表页从索引服务读取而非逐块扫描。合约必须经过Slither静态扫描与Foundry fuzz测试;前端需校验chainId,防止用户在错误网络上签名。上线前建议通过Tenderly做交易模拟,验证gas估算与失败路径。
全栈工程师的价值不在于单点技术深度,而在于让合约、前端、数据层形成闭环,并在每个接缝处做好错误处理与安全假设。以上代码可直接作为项目脚手架起点,按需扩展。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。