Web3/DApp / 联盟链 / 应用场景
Web3 概念与演进
Web1.0 到 Web3.0 演进对比
互联网经历了三个主要阶段的演进,每一阶段在技术架构、所有权模型和用户体验上都有根本性差异。
| 维度 | Web1.0(只读) | Web2.0(读写) | Web3.0(读写+拥有) |
|---|---|---|---|
| 时间 | 1990-2004 | 2004-2020 | 2020 至今 |
| 内容生产 | 只读,仅企业发布内容 | 用户生成内容(UGC) | 用户拥有内容与数据 |
| 网络模型 | 客户端-服务器 | 平台-用户(中心化) | 点对点(去中心化) |
| 数据所有权 | 归平台所有 | 归平台所有 | 归用户所有 |
| 身份系统 | 无统一身份 | 平台账号(OAuth) | 去中心化身份(DID) |
| 支付方式 | 无原生支付 | 第三方支付(支付宝/PayPal) | 原生加密支付 |
| 代表性技术 | HTTP、HTML、门户网站 | AJAX、社交媒体、移动App | 区块链、智能合约、IPFS |
| 代表性产品 | Yahoo、Netscape、新浪 | Facebook、微信、抖音 | Uniswap、OpenSea、ENS |
| 安全性 | 依赖服务器安全 | 平台安全+隐私泄露风险 | 密码学保证+抗审查 |
| 可组合性 | 低 | 中(API 调用) | 高(合约乐高 Lego) |
Web3 核心特征
去中心化(Decentralization)
区块链网络由全球分布的节点共同维护,没有单点故障和控制中心。数据存储在分布式账本上,所有节点持有完整副本,单个节点的退出不影响网络运行。
Web2 架构(中心化): Web3 架构(去中心化):
┌───服务器───┐ ┌───[节点]───┐
│ │ / │ \
┌───┴───┐ ┌───┴───┐ ┌───[节点]───[节点]───[节点]
│ 客户端 │ │ 客户端 │ │ │ │ │ │
└───────┘ └───────┘ [节点]─[节点]─[节点]─[节点]用户主权(User Sovereignty)
用户通过私钥完全控制自己的资产和数据,无需信任第三方。私钥即身份——"Not your keys, not your coins"。用户数据可携带,在不同应用之间无缝迁移。
原生支付(Native Payments)
区块链原生支持价值转移,无需中间机构。智能合约可以自动执行支付逻辑,实现可编程货币。Gas 机制保障网络资源公平使用。
抗审查(Censorship Resistance)
任何实体无法阻止特定地址的交易上链。去中心化应用的前端可通过 IPFS 托管,域名通过 ENS 解析,应用逻辑通过智能合约执行,实现全栈抗审查。
DApp 架构
去中心化应用(DApp)由三层架构组成:
┌─────────────────────────────────────────┐
│ 前端层(Frontend) │
│ React / Vue + ethers.js / wagmi │
│ IPFS 托管 / ENS 域名 │
├─────────────────────────────────────────┤
│ 合约层(Smart Contract) │
│ Solidity / Vyper │
│ EVM 兼容链(Ethereum / Polygon / BSC) │
├─────────────────────────────────────────┤
│ 链下层(Off-chain) │
│ IPFS / Filecoin(存储) │
│ The Graph(索引) │
│ Chainlink(预言机) │
└─────────────────────────────────────────┘前端层:用户交互界面,通过 RPC 节点与区块链通信,使用钱包(如 MetaMask)签署交易。
合约层:智能合约是部署在区块链上的程序,定义了 DApp 的业务逻辑。合约一旦部署不可篡改(或可通过代理合约升级)。
链下层:由于区块链存储成本高昂,大文件(图片、文档)存储在去中心化存储网络(IPFS/Filecoin)中,链上仅存内容哈希(CID)。链上数据通过索引协议(The Graph)高效查询。外部数据通过预言机(Oracle)喂入合约。
DApp 开发框架
Hardhat 项目搭建
Hardhat 是以太坊生态最流行的开发框架,支持编译、测试、部署和调试。
# 初始化项目
mkdir my-dapp && cd my-dapp
npm init -y
npm install --save-dev hardhat @nomicfoundation/hardhat-toolbox
# 初始化 Hardhat 项目
npx hardhat init
# 选择 "Create a JavaScript project" 或 "Create a TypeScript project"项目结构:
my-dapp/
├── contracts/ # Solidity 合约文件
├── test/ # 测试文件
├── scripts/ # 部署脚本
├── hardhat.config.js # 配置文件
├── ignition/ # Hardhat Ignition 部署模块
└── artifacts/ # 编译产物(ABI + Bytecode)Hardhat 配置文件:
// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
module.exports = {
solidity: {
version: "0.8.24",
settings: {
optimizer: { enabled: true, runs: 200 },
},
},
networks: {
hardhat: {
chainId: 31337,
},
sepolia: {
url: process.env.SEPOLIA_RPC_URL || "",
accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [],
},
},
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY || "",
},
};合约编写与测试
示例:ERC20 代币合约
// contracts/MyToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyToken is ERC20, Ownable {
uint256 public constant MAX_SUPPLY = 1_000_000 * 10 ** 18;
constructor() ERC20("MyToken", "MTK") Ownable(msg.sender) {
_mint(msg.sender, 100_000 * 10 ** 18);
}
function mint(address to, uint256 amount) external onlyOwner {
require(totalSupply() + amount <= MAX_SUPPLY, "Exceeds max supply");
_mint(to, amount);
}
function burn(uint256 amount) external {
_burn(msg.sender, amount);
}
}测试用例
// test/MyToken.test.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function () {
let MyToken, myToken, owner, addr1;
beforeEach(async function () {
[owner, addr1] = await ethers.getSigners();
MyToken = await ethers.getContractFactory("MyToken");
myToken = await MyToken.deploy();
await myToken.waitForDeployment();
});
it("should deploy with correct name and symbol", async function () {
expect(await myToken.name()).to.equal("MyToken");
expect(await myToken.symbol()).to.equal("MTK");
});
it("should mint initial supply to owner", async function () {
const ownerBalance = await myToken.balanceOf(owner.address);
expect(ownerBalance).to.equal(ethers.parseEther("100000"));
});
it("should allow owner to mint tokens", async function () {
await myToken.mint(addr1.address, ethers.parseEther("1000"));
expect(await myToken.balanceOf(addr1.address)).to.equal(
ethers.parseEther("1000")
);
});
it("should not allow non-owner to mint", async function () {
await expect(
myToken.connect(addr1).mint(addr1.address, ethers.parseEther("1000"))
).to.be.revertedWithCustomError(myToken, "OwnableUnauthorizedAccount");
});
});运行测试:
npx hardhat test
npx hardhat coverage # 测试覆盖率报告部署脚本
// scripts/deploy.js
const { ethers } = require("hardhat");
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying with account:", deployer.address);
const MyToken = await ethers.getContractFactory("MyToken");
const myToken = await MyToken.deploy();
await myToken.waitForDeployment();
const contractAddress = await myToken.getAddress();
console.log("MyToken deployed to:", contractAddress);
}
main().catch((error) => {
console.error(error);
process.exitCode = 1;
});npx hardhat run scripts/deploy.js --network sepolia前端集成
前端通过 JavaScript 库与智能合约交互,主流方案包括 ethers.js、wagmi 和 viem。
ethers.js
// 使用 ethers.js v6
import { ethers } from "ethers";
// 连接 MetaMask 钱包
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
// 合约 ABI 和地址
const contractAddress = "0x...";
const abi = [
"function balanceOf(address owner) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
"event Transfer(address indexed from, address indexed to, uint256 value)",
];
const contract = new ethers.Contract(contractAddress, abi, signer);
// 读取数据(只读调用,无需 Gas)
const balance = await contract.balanceOf(userAddress);
// 写入数据(需要 Gas,通过钱包签名)
const tx = await contract.transfer(recipientAddress, amount);
await tx.wait(); // 等待交易确认
console.log("Transaction hash:", tx.hash);wagmi + viem (React)
wagmi 是针对 React 的现代 Web3 交互框架,底层使用 viem 代替 ethers.js。
npm install wagmi viem @tanstack/react-query// WagmiProvider.tsx
import { WagmiProvider, createConfig, http } from "wagmi";
import { mainnet, sepolia } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { injected } from "wagmi/connectors";
const config = createConfig({
chains: [mainnet, sepolia],
connectors: [injected()],
transports: {
[mainnet.id]: http(),
[sepolia.id]: http(),
},
});
const queryClient = new QueryClient();
export function Providers({ children }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}// TokenBalance.tsx
import { useAccount, useReadContract, useWriteContract } from "wagmi";
import { formatEther, parseEther } from "viem";
const TOKEN_ABI = [
{
inputs: [{ name: "account", type: "address" }],
name: "balanceOf",
outputs: [{ name: "", type: "uint256" }],
stateMutability: "view",
type: "function",
},
{
inputs: [
{ name: "to", type: "address" },
{ name: "amount", type: "uint256" },
],
name: "transfer",
outputs: [{ name: "", type: "bool" }],
stateMutability: "nonpayable",
type: "function",
},
] as const;
function TokenBalance({ contractAddress }: { contractAddress: `0x${string}` }) {
const { address, isConnected } = useAccount();
const { data: balance, refetch } = useReadContract({
address: contractAddress,
abi: TOKEN_ABI,
functionName: "balanceOf",
args: address ? [address] : undefined,
});
const { writeContract } = useWriteContract();
const handleTransfer = async (to: `0x${string}`, amount: string) => {
await writeContract({
address: contractAddress,
abi: TOKEN_ABI,
functionName: "transfer",
args: [to, parseEther(amount)],
});
};
if (!isConnected) return <div>请连接钱包</div>;
return (
<div>
<p>余额: {balance ? formatEther(balance) : "0"} MTK</p>
</div>
);
}Vue + ethers.js
<!-- TokenCard.vue -->
<script setup lang="ts">
import { ref, onMounted } from "vue";
import { ethers } from "ethers";
const balance = ref<string>("0");
const account = ref<string>("");
const provider = ref<ethers.BrowserProvider | null>(null);
const contractAddress = "0x...";
const abi = [
"function balanceOf(address owner) view returns (uint256)",
"function transfer(address to, uint256 amount) returns (bool)",
];
onMounted(async () => {
if (window.ethereum) {
provider.value = new ethers.BrowserProvider(window.ethereum);
const accounts = await provider.value.send("eth_requestAccounts", []);
account.value = accounts[0];
await fetchBalance();
}
});
async function fetchBalance() {
if (!provider.value || !account.value) return;
const contract = new ethers.Contract(
contractAddress,
abi,
provider.value
);
const bal = await contract.balanceOf(account.value);
balance.value = ethers.formatEther(bal);
}
</script>
<template>
<div class="token-card">
<p>地址: {{ account }}</p>
<p>余额: {{ balance }} MTK</p>
</div>
</template>IPFS 去中心化存储
IPFS(InterPlanetary File System)是一种点对点分布式文件系统,通过内容寻址(CID)替代传统的位置寻址(URL)。
IPFS 核心概念
- CID(Content Identifier):文件内容的哈希值,内容不变则 CID 不变
- MFS(Mutable File System):可变文件系统,类似 Unix 文件目录
- Pinning:固定文件,防止被 GC 回收
- Gateway:通过 HTTP 访问 IPFS 内容(如
https://ipfs.io/ipfs/<CID>)
文件上传示例
// 使用 kubo RPC API 上传文件到 IPFS
import { create } from "kubo-rpc-client";
const ipfs = create({ url: "http://localhost:5001/api/v0" });
async function uploadToIPFS(file) {
const { cid } = await ipfs.add(file, {
pin: true,
});
console.log("File CID:", cid.toString());
return cid.toString();
}
// 上传 JSON 元数据
async function uploadMetadata(metadata) {
const { cid } = await ipfs.add(JSON.stringify(metadata));
return cid.toString();
}Pinata 托管服务
// 使用 Pinata 托管 IPFS 文件
const pinataSDK = require("@pinata/sdk");
const pinata = new pinataSDK(process.env.PINATA_API_KEY, process.env.PINATA_SECRET_KEY);
async function pinFileToIPFS(filePath) {
const fs = require("fs");
const readableStream = fs.createReadStream(filePath);
const result = await pinata.pinFileToIPFS(readableStream, {
pinataMetadata: { name: "my-file" },
});
return `https://gateway.pinata.cloud/ipfs/${result.IpfsHash}`;
}
async function pinJSONToIPFS(json) {
const result = await pinata.pinJSONToIPFS(json);
return `ipfs://${result.IpfsHash}`;
}Filecoin 持久化存储
Filecoin 是 IPFS 的激励层,通过存储证明机制确保数据长期保存。
// 使用 Filecoin 客户端 API
// 安装: npm install @filecoin-shipyard/lotus-client-rpc
const { LotusRPC } = require("@filecoin-shipyard/lotus-client-rpc");
const { NodejsProvider } = require("@filecoin-shipyard/lotus-client-provider-nodejs");
const provider = new NodejsProvider("http://127.0.0.1:1234/rpc/v0");
const client = new LotusRPC(provider);
async function makeDeal(cid, duration) {
// 创建存储交易
const deal = await client.clientStartDeal({
Data: { TransferType: "manual", Root: { "/": cid } },
Wallet: await client.walletDefaultAddress(),
Miner: "f01234", // 矿工 ID
EpochPrice: "250000000", // 每 epoch 价格(attoFIL)
MinBlocksDuration: duration, // 存储时长(epochs)
});
return deal;
}在智能合约中引用 IPFS
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
contract IPFSNFT {
struct Metadata {
string name;
string description;
string imageCID; // IPFS CID
string externalCID;
}
mapping(uint256 => Metadata) public tokenMetadata;
event MetadataUpdated(uint256 indexed tokenId, string imageCID);
function mintWithMetadata(
uint256 tokenId,
string calldata name,
string calldata description,
string calldata imageCID
) external {
tokenMetadata[tokenId] = Metadata(name, description, imageCID, "");
emit MetadataUpdated(tokenId, imageCID);
}
function tokenURI(uint256 tokenId) external view returns (string memory) {
Metadata storage meta = tokenMetadata[tokenId];
return string(abi.encodePacked(
"ipfs://", meta.imageCID
));
}
}The Graph 链上数据索引
The Graph 是一个去中心化索引协议,通过 Subgraph 将链上事件转换为可高效查询的数据。
Subgraph 结构
subgraph/
├── schema.graphql # 数据模型定义
├── src/
│ └── mapping.ts # 事件处理映射
├── subgraph.yaml # 配置文件
├── package.json
└── tsconfig.jsonschema.graphql
type Token @entity {
id: ID!
owner: Bytes!
name: String!
symbol: String!
totalSupply: BigInt!
transfers: [Transfer!]! @derivedFrom(field: "token")
}
type Transfer @entity(immutable: true) {
id: ID!
token: Token!
from: Bytes!
to: Bytes!
amount: BigInt!
timestamp: BigInt!
blockNumber: BigInt!
}subgraph.yaml
specVersion: 1.0.0
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: MyToken
network: sepolia
source:
address: "0x..."
abi: MyToken
startBlock: 1234567
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Token
- Transfer
abis:
- name: MyToken
file: ./abis/MyToken.json
eventHandlers:
- event: Transfer(indexed address,indexed address,uint256)
handler: handleTransfer
file: ./src/mapping.tsmapping.ts
import { BigInt } from "@graphprotocol/graph-ts";
import {
Transfer as TransferEvent,
MyToken,
} from "../generated/MyToken/MyToken";
import { Token, Transfer } from "../generated/schema";
export function handleTransfer(event: TransferEvent): void {
let token = Token.load(event.address.toHexString());
if (!token) {
token = new Token(event.address.toHexString());
let contract = MyToken.bind(event.address);
token.name = contract.name();
token.symbol = contract.symbol();
token.totalSupply = contract.totalSupply();
token.owner = event.params.from;
}
token.save();
let transfer = new Transfer(
event.transaction.hash.concatI32(event.logIndex.toI32())
);
transfer.token = token.id;
transfer.from = event.params.from;
transfer.to = event.params.to;
transfer.amount = event.params.value;
transfer.timestamp = event.block.timestamp;
transfer.blockNumber = event.block.number;
transfer.save();
}前端查询
import { request, gql } from "graphql-request";
const SUBGRAPH_URL = "https://api.studio.thegraph.com/query/<ID>/<SUBGRAPH>/<VERSION>";
const TRANSFERS_QUERY = gql`
query GetTransfers($first: Int!) {
transfers(first: $first, orderBy: timestamp, orderDirection: desc) {
id
from
to
amount
timestamp
}
}
`;
async function getTransfers() {
const data = await request(SUBGRAPH_URL, TRANSFERS_QUERY, { first: 50 });
return data.transfers;
}Chainlink 预言机
预言机(Oracle)将链下数据安全地引入链上。Chainlink 是最大的去中心化预言机网络。
获取价格数据
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
contract PriceConsumer {
AggregatorV3Interface internal priceFeed;
// Sepolia ETH/USD 喂价合约地址
constructor() {
priceFeed = AggregatorV3Interface(
0x694AA1769357215DE4FAC081bf1f309aDC325306
);
}
function getLatestPrice() public view returns (int) {
(
/* uint80 roundID */,
int price,
/* uint startedAt */,
/* uint timeStamp */,
/* uint80 answeredInRound */
) = priceFeed.latestRoundData();
return price / 1e8; // 转换为美元价格
}
}Chainlink VRF(可验证随机函数)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import "@chainlink/contracts/src/v0.8/vrf/VRFConsumerBaseV2.sol";
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol";
contract RandomWinner is VRFConsumerBaseV2 {
VRFCoordinatorV2Interface COORDINATOR;
uint64 private subscriptionId;
bytes32 private keyHash = 0x474e34a077df58807dbe9c96d3c009b23b3c6d0cce433e59bbf5b34f823bc56c;
uint32 private callbackGasLimit = 100000;
mapping(uint256 => address) public requestToSender;
mapping(address => uint256) public randomResults;
event RequestRandom(address indexed sender, uint256 requestId);
event RandomResult(address indexed sender, uint256 randomNumber);
constructor(uint64 _subscriptionId) VRFConsumerBaseV2(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625) {
COORDINATOR = VRFCoordinatorV2Interface(0x8103B0A8A00be2DDC778e6e7eaa21791Cd364625);
subscriptionId = _subscriptionId;
}
function requestRandomNumber() external returns (uint256 requestId) {
requestId = COORDINATOR.requestRandomWords(
keyHash,
subscriptionId,
3, // minimumRequestConfirmations
callbackGasLimit,
1 // numWords
);
requestToSender[requestId] = msg.sender;
emit RequestRandom(msg.sender, requestId);
}
function fulfillRandomWords(uint256 requestId, uint256[] memory randomWords) internal override {
address sender = requestToSender[requestId];
randomResults[sender] = randomWords[0];
emit RandomResult(sender, randomWords[0]);
}
}钱包集成
MetaMask 连接
window.ethereum 基础连接
// 检查 MetaMask 是否安装
if (typeof window.ethereum !== "undefined") {
console.log("MetaMask is installed!");
} else {
console.log("请安装 MetaMask: https://metamask.io");
}
// 请求连接钱包
async function connectWallet() {
try {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const account = accounts[0];
console.log("Connected account:", account);
return account;
} catch (error) {
if (error.code === 4001) {
// EIP-1193 User Rejected Request
console.log("用户拒绝了连接请求");
}
throw error;
}
}
// 监听账户变化
window.ethereum.on("accountsChanged", (accounts) => {
if (accounts.length === 0) {
console.log("钱包已断开连接");
} else {
console.log("账户切换为:", accounts[0]);
}
});
// 监听链切换
window.ethereum.on("chainChanged", (chainId) => {
console.log("链切换为:", parseInt(chainId, 16));
// 建议刷新页面
window.location.reload();
});
// 监听断开连接
window.ethereum.on("disconnect", (error) => {
console.log("钱包断开:", error);
});获取账户与余额
async function getAccountInfo() {
const provider = new ethers.BrowserProvider(window.ethereum);
const signer = await provider.getSigner();
const address = await signer.getAddress();
// 获取 ETH 余额
const balanceWei = await provider.getBalance(address);
const balanceEth = ethers.formatEther(balanceWei);
// 获取链信息
const network = await provider.getNetwork();
const chainId = Number(network.chainId);
return { address, balanceEth, chainId };
}切换链
async function switchChain(chainIdHex) {
try {
await window.ethereum.request({
method: "wallet_switchEthereumChain",
params: [{ chainId: chainIdHex }],
});
} catch (switchError) {
// 如果链不存在,添加链
if (switchError.code === 4902) {
await window.ethereum.request({
method: "wallet_addEthereumChain",
params: [
{
chainId: chainIdHex,
chainName: "Polygon Mainnet",
nativeCurrency: {
name: "MATIC",
symbol: "MATIC",
decimals: 18,
},
rpcUrls: ["https://polygon-rpc.com"],
blockExplorerUrls: ["https://polygonscan.com"],
},
],
});
} else {
throw switchError;
}
}
}
// 切换到 Sepolia 测试网
await switchChain("0xaa36a7"); // chainId 11155111EIP-1193 规范
EIP-1193 定义了以太坊钱包的 JavaScript API 标准,window.ethereum 遵循此规范。
| 方法 | 描述 |
|---|---|
eth_requestAccounts | 请求用户授权访问账户 |
eth_accounts | 返回当前授权的账户列表 |
eth_chainId | 返回当前链 ID |
eth_getBalance | 查询地址余额 |
eth_sendTransaction | 发送交易 |
eth_signTypedData | 结构化数据签名 |
personal_sign | 消息签名 |
wallet_switchEthereumChain | 切换链 |
wallet_addEthereumChain | 添加自定义链 |
// 遵循 EIP-1193 的通用请求封装
class EthereumProvider {
constructor(provider) {
this.provider = provider;
}
async request({ method, params }) {
if (!this.provider) {
throw new Error("Provider not available");
}
try {
const result = await this.provider.request({ method, params });
return result;
} catch (error) {
// EIP-1193 错误格式
const eip1193Error = {
code: error.code,
message: error.message,
data: error.data,
};
throw eip1193Error;
}
}
// 便捷方法
async getAccounts() {
return this.request({ method: "eth_accounts" });
}
async getChainId() {
const chainId = await this.request({ method: "eth_chainId" });
return parseInt(chainId, 16);
}
async getBalance(address) {
return this.request({
method: "eth_getBalance",
params: [address, "latest"],
});
}
}交易签名
eth_sendTransaction
async function sendETH(to, amountInEth) {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const from = accounts[0];
const amountWei = ethers.parseEther(amountInEth);
const txHash = await window.ethereum.request({
method: "eth_sendTransaction",
params: [
{
from,
to,
value: amountWei.toString(16), // 十六进制
gas: "0x5208", // 21000
},
],
});
console.log("Transaction sent:", txHash);
// 等待交易确认
const provider = new ethers.BrowserProvider(window.ethereum);
const receipt = await provider.waitForTransaction(txHash);
console.log("Transaction confirmed:", receipt);
return receipt;
}eth_signTypedData(EIP-712)结构化数据签名
EIP-712 提供结构化、可读的签名数据格式,避免用户误签不可读的哈希。
// 定义 EIP-712 域和类型
const domain = {
name: "MyDApp",
version: "1",
chainId: 1,
verifyingContract: "0x...",
};
const types = {
Order: [
{ name: "seller", type: "address" },
{ name: "buyer", type: "address" },
{ name: "tokenId", type: "uint256" },
{ name: "price", type: "uint256" },
{ name: "deadline", type: "uint256" },
],
};
const message = {
seller: "0x...",
buyer: "0x...",
tokenId: 42,
price: ethers.parseEther("1.5").toString(),
deadline: Math.floor(Date.now() / 1000) + 3600,
};
async function signOrder() {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
// eth_signTypedData_v4 是最新版
const signature = await window.ethereum.request({
method: "eth_signTypedData_v4",
params: [accounts[0], JSON.stringify({ domain, types, message })],
});
console.log("Signature:", signature);
return signature;
}
// 在合约中验证签名// Solidity 端验证 EIP-712 签名
function verifyOrder(
address seller,
address buyer,
uint256 tokenId,
uint256 price,
uint256 deadline,
bytes calldata signature
) external view returns (bool) {
bytes32 digest = _hashTypedDataV4(
keccak256(
abi.encode(
keccak256("Order(address seller,address buyer,uint256 tokenId,uint256 price,uint256 deadline)"),
seller,
buyer,
tokenId,
price,
deadline
)
)
);
address signer = ECDSA.recover(digest, signature);
return signer == seller;
}personal_sign(消息签名)
async function signMessage(message) {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const signature = await window.ethereum.request({
method: "personal_sign",
params: [message, accounts[0]],
});
// 验证签名
const recovered = ethers.verifyMessage(message, signature);
console.log("Expected:", accounts[0]);
console.log("Recovered:", recovered);
return signature;
}Web3Modal 多钱包连接
Web3Modal 是一个统一的钱包连接组件,支持 MetaMask、WalletConnect、Coinbase Wallet 等多种钱包。
npm install @web3modal/wagmi @web3modal/react wagmi viem @tanstack/react-query// 配置 Web3Modal
import { createWeb3Modal } from "@web3modal/wagmi/react";
import { defaultWagmiConfig } from "@web3modal/wagmi/react/config";
import { WagmiProvider } from "wagmi";
import { mainnet, sepolia } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
// 1. 项目配置
const projectId = "YOUR_WALLETCONNECT_PROJECT_ID";
const metadata = {
name: "My DApp",
description: "My Web3 Application",
url: "https://mydapp.com",
icons: ["https://mydapp.com/icon.png"],
};
const chains = [mainnet, sepolia] as const;
const config = defaultWagmiConfig({
chains,
projectId,
metadata,
enableInjected: true,
enableEIP6963: true,
enableCoinbase: true,
});
// 2. 创建 Web3Modal
createWeb3Modal({
wagmiConfig: config,
projectId,
enableAnalytics: true,
});
// 3. 在应用根组件中使用
function App() {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<YourApp />
</QueryClientProvider>
</WagmiProvider>
);
}// Connect Button 组件
import { useWeb3Modal } from "@web3modal/wagmi/react";
import { useAccount, useDisconnect } from "wagmi";
function ConnectButton() {
const { open } = useWeb3Modal();
const { address, isConnected } = useAccount();
const { disconnect } = useDisconnect();
if (isConnected) {
return (
<div>
<span>
{address?.slice(0, 6)}...{address?.slice(-4)}
</span>
<button onClick={() => disconnect()}>断开</button>
<button onClick={() => open({ view: "Account" })}>账户详情</button>
</div>
);
}
return <button onClick={() => open()}>连接钱包</button>;
}联盟链:Hyperledger Fabric
Fabric 架构总览
Hyperledger Fabric 是一种许可型(Permissioned)区块链框架,面向企业级应用。与公链不同,Fabric 的节点需要经过身份认证才能加入网络。
┌───────────────────────────────────────────────────────┐
│ Fabric 网络 │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Org1 │ │ Org2 │ │ Org3 │ │
│ │ ┌──────┐ │ │ ┌──────┐ │ │ ┌──────┐ │ │
│ │ │Peer A│ │ │ │Peer B│ │ │ │Peer C│ │ │
│ │ └──────┘ │ │ └──────┘ │ │ └──────┘ │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ │ │ │ │
│ ┌────┴──────────────┴──────────────┴────┐ │
│ │ Orderer 节点 │ │
│ │ (Raft 共识 / Kafka / PBFT) │ │
│ └───────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────┐ │
│ │ Fabric CA │ │
│ │ (证书颁发 / 身份管理 / MSP) │ │
│ └───────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘核心组件与角色
Peer 节点(记账节点)
Peer 节点维护账本副本,执行链码(Chaincode),并验证交易。每个 Peer 属于一个组织(Org),一个组织可以有多个 Peer。
- 背书节点(Endorsing Peer):接收客户端交易提案,模拟执行链码,返回背书签名
- 记账节点(Committing Peer):验证 Orderer 发来的区块,将有效区块追加到账本
Orderer 节点(排序节点)
Orderer 节点对交易进行排序并打包成区块,负责共识机制。Orderer 不维护世界状态,不执行链码。常用的共识方式:
- Raft:CFT(Crash Fault Tolerance),生产环境推荐
- Kafka:已弃用,由 Raft 替代
- PBFT:BFT(Byzantine Fault Tolerance),需要额外配置
CA(Certificate Authority,证书颁发机构)
Fabric CA 负责颁发和管理 TLS 证书与 MSP 证书,是权限控制的基础。每个组织有独立的 CA,颁发成员证书和 TLS 证书。
通道(Channel)
通道是 Fabric 的私密通信机制,通道内的 Peer 共享一个独立的账本。通道实现了数据隔离——不在通道内的节点无法访问通道数据。
┌────────────────── Fabric 网络 ──────────────────┐
│ │
│ ┌────────── Channel A ──────────┐ │
│ │ Org1.Peer1, Org2.Peer1 │ │
│ │ 账本 A / 链码 A │ │
│ └──────────────────────────────┘ │
│ │
│ ┌────────── Channel B ──────────┐ │
│ │ Org2.Peer2, Org3.Peer1 │ │
│ │ 账本 B / 链码 B │ │
│ └──────────────────────────────┘ │
│ │
└───────────────────────────────────────────────────┘链码(Chaincode)
链码是 Fabric 中的智能合约,可以用 Go、Java 或 JavaScript 编写。链码通过 Docker 容器隔离运行。
通道与私有数据
私有数据集合(Private Data Collection)
当需要在通道内部分组织间共享敏感数据时,使用私有数据集合。私有数据只在授权节点间通过 gossip 传输,不在通道所有节点间广播。
# collections_config.json
[
{
"name": "privateDetails",
"policy": "OR('Org1MSP.member', 'Org2MSP.member')",
"requiredPeerCount": 0,
"maxPeerCount": 3,
"blockToLive": 0,
"memberOnlyRead": true
}
]// 链码中访问私有数据
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface,
id string, publicData string, privateData string) error {
// 公开数据写入状态
err := ctx.GetStub().PutState(id, []byte(publicData))
if err != nil {
return err
}
// 私有数据写入集合
err = ctx.GetStub().PutPrivateData("privateDetails", id, []byte(privateData))
if err != nil {
return err
}
return nil
}
func (s *SmartContract) GetPrivateData(ctx contractapi.TransactionContextInterface,
id string) (string, error) {
privateBytes, err := ctx.GetStub().GetPrivateData("privateDetails", id)
if err != nil {
return "", fmt.Errorf("failed to get private data: %v", err)
}
return string(privateBytes), nil
}Fabric 链码开发(Go 语言)
项目结构
chaincode/
├── go.mod
├── go.sum
├── main.go
├── chaincode/
│ └── asset.go # 链码逻辑
├── META-INF/
│ └── statedb/
│ └── couchdb/
│ └── indexes/ # CouchDB 索引定义
└── vendor/go.mod
module chaincode
go 1.21
require (
github.com/hyperledger/fabric-chaincode-go v0.0.0-20240112141011-e1a6bd5b2e6c
github.com/hyperledger/fabric-contract-api-go v1.2.2
)资产链码示例
// chaincode/asset.go
package main
import (
"encoding/json"
"fmt"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
)
// Asset 定义资产结构
type Asset struct {
ID string `json:"id"`
Owner string `json:"owner"`
Value int `json:"value"`
Color string `json:"color"`
Size int `json:"size"`
AppraisedValue int `json:"appraisedValue"`
}
// SmartContract 合约结构体
type SmartContract struct {
contractapi.Contract
}
// CreateAsset 创建资产
func (s *SmartContract) CreateAsset(ctx contractapi.TransactionContextInterface,
id string, owner string, value int, color string, size int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if exists {
return fmt.Errorf("asset %s already exists", id)
}
asset := Asset{
ID: id,
Owner: owner,
Value: value,
Color: color,
Size: size,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// ReadAsset 读取资产
func (s *SmartContract) ReadAsset(ctx contractapi.TransactionContextInterface,
id string) (*Asset, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return nil, fmt.Errorf("failed to read asset %s: %v", id, err)
}
if assetJSON == nil {
return nil, fmt.Errorf("asset %s does not exist", id)
}
var asset Asset
err = json.Unmarshal(assetJSON, &asset)
if err != nil {
return nil, err
}
return &asset, nil
}
// UpdateAsset 更新资产
func (s *SmartContract) UpdateAsset(ctx contractapi.TransactionContextInterface,
id string, owner string, value int, color string, size int) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("asset %s does not exist", id)
}
asset := Asset{
ID: id,
Owner: owner,
Value: value,
Color: color,
Size: size,
}
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// DeleteAsset 删除资产
func (s *SmartContract) DeleteAsset(ctx contractapi.TransactionContextInterface,
id string) error {
exists, err := s.AssetExists(ctx, id)
if err != nil {
return err
}
if !exists {
return fmt.Errorf("asset %s does not exist", id)
}
return ctx.GetStub().DelState(id)
}
// AssetExists 检查资产是否存在
func (s *SmartContract) AssetExists(ctx contractapi.TransactionContextInterface,
id string) (bool, error) {
assetJSON, err := ctx.GetStub().GetState(id)
if err != nil {
return false, fmt.Errorf("failed to read asset %s: %v", id, err)
}
return assetJSON != nil, nil
}
// TransferAsset 资产转移
func (s *SmartContract) TransferAsset(ctx contractapi.TransactionContextInterface,
id string, newOwner string) error {
asset, err := s.ReadAsset(ctx, id)
if err != nil {
return err
}
asset.Owner = newOwner
assetJSON, err := json.Marshal(asset)
if err != nil {
return err
}
return ctx.GetStub().PutState(id, assetJSON)
}
// GetAllAssets 查询所有资产
func (s *SmartContract) GetAllAssets(ctx contractapi.TransactionContextInterface) ([]*Asset, error) {
resultsIterator, err := ctx.GetStub().GetStateByRange("", "")
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}
return assets, nil
}
// 富查询(需要 CouchDB 状态数据库)
func (s *SmartContract) GetAssetsByOwner(ctx contractapi.TransactionContextInterface,
owner string) ([]*Asset, error) {
queryString := fmt.Sprintf(`{"selector":{"owner":"%s"}}`, owner)
resultsIterator, err := ctx.GetStub().GetQueryResult(queryString)
if err != nil {
return nil, err
}
defer resultsIterator.Close()
var assets []*Asset
for resultsIterator.HasNext() {
queryResponse, err := resultsIterator.Next()
if err != nil {
return nil, err
}
var asset Asset
err = json.Unmarshal(queryResponse.Value, &asset)
if err != nil {
return nil, err
}
assets = append(assets, &asset)
}
return assets, nil
}
// main.go
func main() {
chaincode, err := contractapi.NewChaincode(&SmartContract{})
if err != nil {
fmt.Printf("Error creating chaincode: %s", err.Error())
return
}
if err := chaincode.Start(); err != nil {
fmt.Printf("Error starting chaincode: %s", err.Error())
}
}链码测试
// chaincode/asset_test.go
package main
import (
"encoding/json"
"testing"
"github.com/hyperledger/fabric-chaincode-go/shim"
"github.com/hyperledger/fabric-contract-api-go/contractapi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockStub 模拟存根
type MockStub struct {
shim.ChaincodeStubInterface
mock.Mock
state map[string][]byte
}
func TestCreateAsset(t *testing.T) {
chaincode, ctx := getChaincodeAndCtx()
err := chaincode.CreateAsset(ctx, "asset1", "owner1", 100, "blue", 10)
assert.NoError(t, err)
result, err := chaincode.ReadAsset(ctx, "asset1")
assert.NoError(t, err)
assert.Equal(t, "asset1", result.ID)
assert.Equal(t, "owner1", result.Owner)
}
func TestTransferAsset(t *testing.T) {
chaincode, ctx := getChaincodeAndCtx()
err := chaincode.CreateAsset(ctx, "asset1", "owner1", 100, "blue", 10)
assert.NoError(t, err)
err = chaincode.TransferAsset(ctx, "asset1", "owner2")
assert.NoError(t, err)
result, err := chaincode.ReadAsset(ctx, "asset1")
assert.NoError(t, err)
assert.Equal(t, "owner2", result.Owner)
}
func getChaincodeAndCtx() (*SmartContract, contractapi.TransactionContextInterface) {
// 实际测试中使用 fabric-sdk-go 提供的测试框架
return &SmartContract{}, nil
}Java 链码示例
// AssetContract.java
package org.example;
import org.hyperledger.fabric.contract.Context;
import org.hyperledger.fabric.contract.ContractInterface;
import org.hyperledger.fabric.contract.annotation.*;
import org.hyperledger.fabric.shim.ChaincodeException;
import org.hyperledger.fabric.shim.ChaincodeStub;
import com.owlike.genson.Genson;
@Contract(name = "AssetContract",
transaction = @Transaction(required = true))
public class AssetContract implements ContractInterface {
private final Genson genson = new Genson();
@Transaction(intent = Transaction.TYPE.SUBMIT)
public Asset createAsset(final Context ctx, final String id,
final String owner, final int value) {
ChaincodeStub stub = ctx.getStub();
String assetState = stub.getStringState(id);
if (!assetState.isEmpty()) {
String errorMessage = String.format("Asset %s already exists", id);
throw new ChaincodeException(errorMessage);
}
Asset asset = new Asset(id, owner, value);
String assetJson = genson.serialize(asset);
stub.putStringState(id, assetJson);
return asset;
}
@Transaction(intent = Transaction.TYPE.EVALUATE)
public Asset readAsset(final Context ctx, final String id) {
ChaincodeStub stub = ctx.getStub();
String assetJson = stub.getStringState(id);
if (assetJson.isEmpty()) {
String errorMessage = String.format("Asset %s does not exist", id);
throw new ChaincodeException(errorMessage);
}
return genson.deserialize(assetJson, Asset.class);
}
}Fabric CA 证书管理
# 启动 Fabric CA 服务器
fabric-ca-server start -b admin:adminpw -p 7054
# 注册新用户
fabric-ca-client register --id.name user1 --id.type client \
--id.affiliation org1.department1 --id.attrs 'role=client:ecert'
# 登记用户(获取证书)
fabric-ca-client enroll -u http://user1:user1pw@localhost:7054 \
-M $FABRIC_CFG_PATH/crypto-config/peerOrganizations/org1.example.com/users/user1/msp
# 获取 TLS 证书
fabric-ca-client enroll -u http://admin:adminpw@localhost:7054 \
--enrollment.profile tls -M $FABRIC_CFG_PATH/crypto-config/ordererOrganizations/example.com/orderers/orderer.example.com/tls