Step1:环境配置
1.下载git,并且配置到环境变量
2.下载npm(nvm进行版本管理,方便后续不同项目需要不同的版本)
3.下载Nodejs
4.下载go环境,并且配置环境变量
5.下载gcc(c语言编译环境,并且配置环境变量)
6.访问https://github.com/ethereum/go-ethereum,(Geth)下载以太坊源码,并且编译(编译成功会在根目录下产生build文件夹->bin目录,里面有可执行的geth.exe文件),本次实习要求geth的版本为1.10.1
./geth version验证版本,若版本不正确,可根据以下步骤切换:
git fetch –tags 拉取所有标签,确保本地有1.10.1
git checkout v1.10.1切换到1.10.1
make clean 清理之前的编译结果
make geth 重新编译geth 或者运行go run build/ci.go install,也可以用go进行编译项目
./geth version重新再次验证版本,这一步记得在Bin目录之下执行
Step2:搭建私有链
1.终端运行ipconfig,查找自己本机ipv4地址
2.在bin目录下创建初始化文件,名为genesis.json,其中放入如下代码:
{
“config”: {
“chainId”: 12346, //这个是自己的私有链id,记住
“homesteadBlock”: 0,
“eip150Block”: 0,
“eip155Block”: 0,
“eip158Block”: 0,
“byzantiumBlock”: 0,
“constantinopleBlock”: 0,
“petersburgBlock”: 0,
“istanbulBlock”: 0,
“berlinBlock”: 0,
“londonBlock”: 0
},
“alloc”: {},
“coinbase”: “0x0000000000000000000000000000000000000000”,
“difficulty”: “0x400”,
“extraData”: “”,
“gasLimit”: “0xffffffff”,
“nonce”: “0x0000000000000042”,
“mixhash”: “0x0000000000000000000000000000000000000000000000000000000000000000”,
“parentHash”: “0x0000000000000000000000000000000000000000000000000000000000000000”,
“timestamp”: “0x00”
}
3.初始化私有链,终端执行
geth init genesis.json
或者指定数据目录:
geth –datadir ./privatechain init genesis.json
4.在bin目录下打开终端,运行
.\\geth.exe –datadir private-chain-data `
–networkid 12346 –port 30303
–http –http.addr 0.0.0.0 –http.port 8545 –http.api "web3,eth,net,admin,personal,miner"
–nodiscover –nat extip:101.7.169.31
console
其中101.7.169.31替换为自己的本机ip地址,这几行命令是用于搭建私有链的创世节点,并且启动,启动成功应该显示如下信息:
注意执行这一步时,8545等端口不要被占用,其中即使你执行netstat -ano | findstr :8545,并不代表端口一定空闲,该端口可能被docker与分配了,如果是这样,需要先退出docker,重启电脑,再执行就可以了
在此界面下,运行miner.start(1)可以开始挖矿(挖矿成功会显示小锤子),miner.stop()停止挖矿,geth.accounts查看所有账户,eth.getBalance(eth.accounts[0])查看第0个账户的余额,personal.newAccount(“你的密码”)创建新账户,等等,具体详细查询这几个1命令的参数:
eth:区块链相关操作
net:查看 P2P 网络状态
admin:管理节点
miner:启动 & 停止挖矿
personal:管理账户
txpool:查看交易内存池
web3:单位换算等
其中要成链起码需要两个节点,要使别人加入你的节点,你需要先运行admin.nodeInfo.enode查看自己的enode,然后令其他节点拥有和你相同的genesis.json文件,并且同样用该文件初始化,然后其他节点也在Bin目录下运行:
.\\geth.exe –datadir node-data –networkid 12346
–port 30304 –syncmode "fast"
–bootnodes “enode://7795957233ae07a81cd06be36e5a3d2a5fa8e6d2ca832f3c68c23273af185cd3d56c35ba24c1fee2d32b6db43c7b8f9fe07272c78d1fad40ac1387d35bf74aab@101.7.169.31:30304” `
console
启动之后运行admin.addPeer(“enode://7795957233ae07a81cd06be36e5a3d2a5fa8e6d2ca832f3c68c23273af185cd3d56c35ba24c1fee2d32b6db43c7b8f9fe07272c78d1fad40ac1387d35bf74aab@101.7.169.31:30304”)加入你的链,然后可以用admin.peers,如果成功加入,会显示

如果没有成功则显示一个空数组[],
私有链搭建成功后可以执行转账等操作,转账需要用到eth,accounts查询账户地址,0x开头的16进制数字,可以写转账脚本来运行(图中transfer.js文件)

Setp3:搭建以太坊私有链浏览器
方法一:github找现有开源项目下载编译运行
这里我以github.com/carsenk/explorer为例,因为它比较简单,适合私有链部署。
Fork项目到自己的GitHub账号
打开项目主页github.com/carsenk/explorer。
点击右上角的"Fork"按钮,项目就会复制到你的账号下
克隆项目到本地
git clone https://github.com/你的用户名/explorer.git
npm install安装依赖,npm start或者npm run dev启动项目
方法二:自己搭建前后端构建(这里我用vue+nodejs来构建)
首先用vite脚手架来创建一个vue项目,并且安装pinia实现状态管理
后端:
环境配置:1…env

必须一字不差,不能有多余的符号
2.server.js
// backend/server.js – ES模块版本
import express from 'express'
import cors from 'cors'
import { ethers } from 'ethers'
import dotenv from 'dotenv'
dotenv.config()
const app = express()
const PORT = process.env.PORT || 3001
// 中间件
app.use(cors())
app.use(express.json())
// 配置
const RPC_URL = process.env.RPC_URL || 'http://localhost:8545'
const provider = new ethers.JsonRpcProvider(RPC_URL)
// 健康检查
app.get('/api/health', (req, res) => {
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
service: '以太坊区块链API',
version: '1.0.0'
})
})
// 获取网络信息
app.get('/api/network', async (req, res) => {
try {
const network = await provider.getNetwork()
const blockNumber = await provider.getBlockNumber()
res.json({
success: true,
data: {
chainId: Number(network.chainId),
chainName: network.name,
blockNumber: Number(blockNumber),
rpcUrl: RPC_URL
}
})
} catch (error) {
res.status(500).json({
success: false,
error: error.message
})
}
})
// 获取第一个账户
// 先找到原来的 /api/account/first 接口,替换成这个:
//这个接口必须写在address之前,否则first会被当做address的参数使用
app.get('/api/account/first', async (req, res) => {
try {
let defaultAccount = (process.env.DEFAULT_ACCOUNT || "0xe415dfc8eaf5743dd711d88a6add44e533ecfbfa").trim();
defaultAccount = "0xe415dfc8eaf5743dd711d88a6add44e533ecfbfa"; // 写自己默认账户地址
const ethAddressRegex = /^0x[a-fA-F0-9]{40}$/;
if (!ethAddressRegex.test(defaultAccount)) {
return res.status(400).json({
success: false,
error: `默认账户地址无效(当前值:${defaultAccount})`
});
}
const balance = await provider.getBalance(defaultAccount);
const txCount = await provider.getTransactionCount(defaultAccount);
res.json({
success: true,
data: {
address: defaultAccount,
balance: ethers.formatEther(balance),
balanceWei: balance.toString(),
transactionCount: txCount,
isContract: false,
isDefaultAccount: true,
lastUpdated: new Date().toISOString()
}
});
} catch (error) {
res.status(500).json({
success: false,
error: error.message
});
}
});
app.get('/api/account/:address', async (req, res) => {
try {
const { address } = req.params
if (!address || !address.startsWith('0x')) {
return res.status(400).json({
success: false,
error: '无效的地址格式'
})
}
const balance = await provider.getBalance(address)
const txCount = await provider.getTransactionCount(address)
res.json({
success: true,
data: {
address: address,
balance: ethers.formatEther(balance),
balanceWei: balance.toString(),
transactionCount: txCount,
isContract: false,
lastUpdated: new Date().toISOString()
}
})
} catch (error) {
res.status(500).json({
success: false,
error: error.message
})
}
})
前端
1.src下services文件夹
api.js
const API_BASE = import.meta.env.VITE_APP_API_URL
class ApiService {
constructor() {
this.baseUrl = API_BASE
}
async getHealth() {
const response = await fetch(`${this.baseUrl}/health`)
return response.json()
}
async getNetwork() {
const response = await fetch(`${this.baseUrl}/network`)
return response.json()
}
async getAccount(address) {
const response = await fetch(`${this.baseUrl}/account/${address}`)
return response.json()
}
async getFirstAccount() {
const response = await fetch(`${this.baseUrl}/account/first`)
return response.json()
}
async getAccountsBalance(addresses) {
const response = await fetch(`${this.baseUrl}/accounts/balance`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ addresses })
})
return response.json()
}
}
const apiService = new ApiService()
export default apiService
ethereum.js
import { ethers } from 'ethers'
class EthereumService {
constructor() {
this.rpcUrl = import.meta.env.VITE_APP_RPC_URL || 'http://localhost:8545'
this.chainId = parseInt(import.meta.env.VITE_APP_CHAIN_ID || '12346')
this.chainName = import.meta.env.VITE_APP_CHAIN_NAME || '私有链'
this.provider = new ethers.JsonRpcProvider(this.rpcUrl, {
chainId: this.chainId,
name: this.chainName
})
}
async getNetworkInfo() {
try {
const network = await this.provider.getNetwork()
const blockNumber = await this.provider.getBlockNumber()
return {
chainId: network.chainId,
name: network.name,
rpcUrl: this.rpcUrl,
blockNumber: blockNumber,
chainName: this.chainName
}
} catch (error) {
console.error('获取网络信息失败:', error)
return {
chainId: this.chainId,
name: this.chainName,
rpcUrl: this.rpcUrl,
blockNumber: 0,
error: error.message
}
}
}
async testConnection() {
try {
await this.provider.getBlockNumber()
return { connected: true, message: '连接成功' }
} catch (error) {
return { connected: false, message: '连接失败: ' + error.message }
}
}
}
const ethereumService = new EthereumService()
export default ethereumService
stores:
account.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import apiService from '@/services/api'
export const useAccountStore = defineStore('account', () => {
const address = ref('')
const balance = ref('0')
const balanceWei = ref('0')
const transactionCount = ref(0)
const isContract = ref(false)
const isDefaultAccount = ref(false)
const lastUpdated = ref(null)
const loading = ref(false)
const error = ref(null)
const pollingInterval = ref(null)
const formattedBalance = computed(() => {
const bal = parseFloat(balance.value)
if (bal === 0) return '0 ETH'
if (bal < 0.001) return '< 0.001 ETH'
if (bal < 1) return bal.toFixed(4) + ' ETH'
if (bal < 1000) return bal.toFixed(2) + ' ETH'
return (bal / 1000).toFixed(1) + ' K ETH'
})
const shortAddress = computed(() => {
if (!address.value || address.value.length < 10) return address.value
return `${address.value.substring(0, 6)}…${address.value.substring(address.value.length – 4)}`
})
const isConnected = computed(() => {
return !!address.value && !error.value
})
async function fetchAccountInfo(accountAddress = null) {
loading.value = true
error.value = null
try {
let data
if (accountAddress) {
// 获取指定账户
const response = await apiService.getAccount(accountAddress)
if (!response.success) {
throw new Error(response.error || '获取账户信息失败')
}
data = response.data
address.value = accountAddress
} else {
// 获取第一个账户
const response = await apiService.getFirstAccount()
if (!response.success) {
throw new Error(response.error || '获取第一个账户失败')
}
data = response.data
if (!data.address || !data.address.startsWith('0x')) {
throw new Error('默认账户地址格式无效')
}
address.value = data.address
}
balance.value = data.balance
balanceWei.value = data.balanceWei
transactionCount.value = data.transactionCount
isContract.value = data.isContract
isDefaultAccount.value = data.isDefaultAccount || false
lastUpdated.value = new Date(data.lastUpdated)
} catch (err) {
error.value = err.message
console.error('获取账户信息失败:', err)
} finally {
loading.value = false
}
}
function startPolling(interval = 5000) {
stopPolling() // 先停止现有的轮询
fetchAccountInfo(address.value || null)
pollingInterval.value = setInterval(() => {
if (address.value) {
fetchAccountInfo(address.value)
}
}, interval)
return pollingInterval.value
}
function stopPolling() {
if (pollingInterval.value) {
clearInterval(pollingInterval.value)
pollingInterval.value = null
}
}
async function refreshBalance() {
await fetchAccountInfo(address.value)
}
async function copyAddress() {
if (!address.value) return false
try {
await navigator.clipboard.writeText(address.value)
return true
} catch (err) {
console.error('复制失败:', err)
return false
}
}
return {
address,
balance,
balanceWei,
transactionCount,
isContract,
isDefaultAccount,
lastUpdated,
loading,
error,
formattedBalance,
shortAddress,
isConnected,
fetchAccountInfo,
startPolling,
stopPolling,
refreshBalance,
copyAddress
}
})
network.js
import { defineStore } from 'pinia'
import { ref } from 'vue'
import ethereumService from '@/services/ethereum'
export const useNetworkStore = defineStore('network', () => {
const chainName = ref('')
const chainId = ref('')
const blockNumber = ref(0)
const rpcUrl = ref('')
const connected = ref(false)
const loading = ref(false)
const error = ref(null)
async function fetchNetworkInfo() {
loading.value = true
error.value = null
try {
const info = await ethereumService.getNetworkInfo()
chainName.value = info.name || info.chainName
chainId.value = info.chainId
blockNumber.value = info.blockNumber
rpcUrl.value = info.rpcUrl
connected.value = true
} catch (err) {
error.value = err.message
connected.value = false
} finally {
loading.value = false
}
}
async function testConnection() {
loading.value = true
const result = await ethereumService.testConnection()
connected.value = result.connected
loading.value = false
return result
}
return {
// 状态
chainName,
chainId,
blockNumber,
rpcUrl,
connected,
loading,
error,
fetchNetworkInfo,
testConnection
}
})
App.vue
<template>
<div id="app">
<div class="container">
<!– 标题 –>
<header class="header">
<h1>🚀 以太坊私有链浏览器</h1>
<p class="subtitle">实时账户余额监控</p>
</header>
<!– 主内容 –>
<main class="main">
<!– 网络信息卡片 –>
<el-card class="network-card" shadow="hover">
<template #header>
<div class="card-header">
<h2>网络信息</h2>
<el-button
type="primary"
size="small"
:loading="networkStore.loading"
@click="networkStore.fetchNetworkInfo"
>
刷新
</el-button>
</div>
</template>
<!– 状态指示器 –>
<div class="status-indicator">
<el-tag
:type="networkStore.connected ? 'success' : 'danger'"
size="large"
>
<el-icon v-if="networkStore.connected">
<CircleCheck />
</el-icon>
<el-icon v-else>
<CircleClose />
</el-icon>
{{ networkStore.connected ? '已连接' : '未连接' }}
</el-tag>
</div>
<!– 链名称 –>
<div class="chain-name-section">
<h3>链的名称:</h3>
<div class="chain-name-display">
<el-text type="primary" size="large">
<strong>{{ networkStore.chainName || '正在获取…' }}</strong>
</el-text>
</div>
</div>
<!– 网络详情 –>
<div class="details">
<div class="detail-item">
<span class="label">链ID:</span>
<span class="value">{{ networkStore.chainId || '–' }}</span>
</div>
<div class="detail-item">
<span class="label">当前区块:</span>
<span class="value">{{ networkStore.blockNumber.toLocaleString() || '–' }}</span>
</div>
<div class="detail-item">
<span class="label">RPC节点:</span>
<span class="value">{{ networkStore.rpcUrl || '–' }}</span>
</div>
</div>
</el-card>
<!– 账户余额卡片 –>
<el-card class="account-card" shadow="hover">
<template #header>
<div class="card-header">
<h2>账户余额</h2>
<div class="header-actions">
<el-switch
v-model="autoRefresh"
active-text="自动更新"
inactive-text="手动更新"
@change="toggleAutoRefresh"
/>
<el-button
type="primary"
size="small"
:loading="accountStore.loading"
@click="accountStore.refreshBalance"
>
刷新余额
</el-button>
</div>
</div>
</template>
<!– 账户地址 –>
<div class="account-address-section">
<h3>账户地址:</h3>
<div class="address-display">
<el-text type="primary" size="large" class="address-text">
<strong>{{ accountStore.address || '无账户' }}</strong>
</el-text>
<el-button
v-if="accountStore.address"
type="text"
@click="copyAccountAddress"
:title="'点击复制: ' + accountStore.address"
>
<el-icon>
<CopyDocument />
</el-icon>
复制
</el-button>
</div>
<div v-if="accountStore.shortAddress" class="short-address">
短格式: {{ accountStore.shortAddress }}
</div>
</div>
<!– 余额信息 –>
<div class="balance-section">
<div class="balance-display">
<div class="balance-label">余额:</div>
<div class="balance-value">
<span class="balance-amount">{{ accountStore.formattedBalance }}</span>
<span class="balance-unit">ETH</span>
</div>
<div class="balance-detail">
精确值: {{ accountStore.balance }} ETH
</div>
</div>
</div>
<!– 账户详情 –>
<div class="account-details">
<div class="detail-grid">
<div class="detail-item">
<span class="label">交易数量:</span>
<span class="value">{{ accountStore.transactionCount }}</span>
</div>
<div class="detail-item">
<span class="label">账户类型:</span>
<span class="value">
<el-tag :type="accountStore.isContract ? 'warning' : 'success'" size="small">
{{ accountStore.isContract ? '合约账户' : '外部账户' }}
</el-tag>
</span>
</div>
<div class="detail-item">
<span class="label">默认账户:</span>
<span class="value">
<el-tag :type="accountStore.isDefaultAccount ? 'success' : 'info'" size="small">
{{ accountStore.isDefaultAccount ? '是' : '否' }}
</el-tag>
</span>
</div>
<div class="detail-item">
<span class="label">最后更新:</span>
<span class="value">
{{ formatTime(accountStore.lastUpdated) }}
</span>
</div>
</div>
</div>
<!– 错误信息 –>
<div v-if="accountStore.error" class="error-message">
<el-alert
:title="`账户信息错误: ${accountStore.error}`"
type="error"
:closable="false"
/>
</div>
<!– 加载状态 –>
<div v-if="accountStore.loading" class="loading-indicator">
<el-progress :percentage="50" status="success" :show-text="false" />
<div class="loading-text">正在获取账户信息…</div>
</div>
</el-card>
<!– 控制面板 –>
<el-card class="control-card" shadow="never">
<template #header>
<h3>控制面板</h3>
</template>
<div class="controls">
<div class="control-group">
<h4>账户查询</h4>
<div class="address-input">
<el-input
v-model="customAddress"
placeholder="输入以太坊地址 (0x开头)"
:disabled="accountStore.loading"
>
<template #append>
<el-button
@click="queryCustomAddress"
:loading="accountStore.loading"
>
查询
</el-button>
</template>
</el-input>
</div>
<div class="control-buttons">
<el-button
type="primary"
@click="useDefaultAccount"
:loading="accountStore.loading"
>
使用默认账户
</el-button>
</div>
</div>
</div>
</el-card>
<el-card class="instructions-card" shadow="never">
<template #header>
<h3>使用说明</h3>
</template>
<div class="instructions">
<p>1. 确保后端服务已启动: <code>node backend/server.js</code></p>
<p>2. 确保Geth节点正在运行并开放8545端口</p>
<p>3. 默认账户在 <code>backend/.env</code> 中配置</p>
<p>4. 开启"自动更新"可实时监控余额变化</p>
<p>5. 可输入任意以太坊地址查询余额</p>
</div>
</el-card>
</main>
<!– 页脚 –>
<footer class="footer">
<p>© 2023 以太坊私有链浏览器 | 后端状态: {{ backendStatus }} | 账户: {{ accountStore.shortAddress || '无' }}</p>
</footer>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useNetworkStore } from '@/stores/network'
import { useAccountStore } from '@/stores/account'
import { CircleCheck, CircleClose, CopyDocument } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
const networkStore = useNetworkStore()
const accountStore = useAccountStore()
const autoRefresh = ref(true)
const customAddress = ref('')
const backendStatus = ref('未知')
const formatTime = (date) => {
if (!date) return '–'
return new Date(date).toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
const copyAccountAddress = async () => {
const success = await accountStore.copyAddress()
if (success) {
ElMessage.success('地址已复制到剪贴板')
} else {
ElMessage.error('复制失败')
}
}
const toggleAutoRefresh = (value) => {
if (value) {
accountStore.startPolling(5000)
ElMessage.success('已开启自动更新')
} else {
accountStore.stopPolling()
ElMessage.info('已关闭自动更新')
}
}
const queryCustomAddress = async () => {
if (!customAddress.value || !customAddress.value.startsWith('0x')) {
ElMessage.warning('请输入有效的以太坊地址 (0x开头)')
return
}
await accountStore.fetchAccountInfo(customAddress.value)
if (!accountStore.error) {
ElMessage.success('账户信息已更新')
}
customAddress.value = ''
}
const useDefaultAccount = async () => {
await accountStore.fetchAccountInfo()
if (!accountStore.error) {
ElMessage.success('已切换到默认账户')
}
}
const checkBackendStatus = async () => {
try {
const response = await fetch(`${import.meta.env.VITE_APP_API_URL}/health`)
if (response.ok) {
backendStatus.value = '正常'
} else {
backendStatus.value = '异常'
}
} catch (error) {
backendStatus.value = '离线'
}
}
onMounted(async () => {
await checkBackendStatus()
await networkStore.fetchNetworkInfo()
await accountStore.fetchAccountInfo()
if (autoRefresh.value) {
accountStore.startPolling(5000)
}
})
onUnmounted(() => {
accountStore.stopPolling()
})
</script>
<style scoped>
#app {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 20px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
}
.container {
max-width: 1000px;
margin: 0 auto;
min-height: 100vh;
display: flex;
flex-direction: column;
}
.header {
text-align: center;
color: white;
margin-bottom: 30px;
padding: 20px 0;
}
.header h1 {
font-size: 2.5rem;
margin: 0;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.header .subtitle {
font-size: 1.2rem;
opacity: 0.9;
margin-top: 10px;
}
.main {
flex: 1;
display: flex;
flex-direction: column;
gap: 20px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 0 10px 0;
border-bottom: 1px solid #f0f0f0;
}
.card-header h2 {
margin: 0;
color: #333;
font-size: 1.5rem;
}
.header-actions {
display: flex;
align-items: center;
gap: 10px;
}
.status-indicator {
text-align: center;
margin: 20px 0;
}
.chain-name-section, .account-address-section {
text-align: center;
margin: 20px 0;
padding: 20px;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
border-radius: 8px;
border-left: 4px solid #409eff;
}
.chain-name-section h3, .account-address-section h3 {
margin: 0 0 10px 0;
color: #666;
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 1px;
}
.chain-name-display {
font-size: 2rem;
color: #1890ff;
font-weight: 700;
text-shadow: 0 2px 4px rgba(24, 144, 255, 0.2);
}
.account-address-section .address-display {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-bottom: 10px;
}
.address-text {
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 1.2rem;
word-break: break-all;
}
.short-address {
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 0.9rem;
color: #666;
background: rgba(0, 0, 0, 0.05);
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.balance-section {
text-align: center;
margin: 30px 0;
padding: 30px 20px;
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-radius: 12px;
color: white;
}
.balance-display {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
}
.balance-label {
font-size: 1.2rem;
opacity: 0.9;
}
.balance-value {
display: flex;
align-items: baseline;
gap: 5px;
}
.balance-amount {
font-size: 3.5rem;
font-weight: 700;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.balance-unit {
font-size: 2rem;
opacity: 0.9;
}
.balance-detail {
font-size: 0.9rem;
opacity: 0.8;
background: rgba(255, 255, 255, 0.1);
padding: 4px 12px;
border-radius: 20px;
}
.details, .account-details {
margin: 20px 0;
padding: 20px;
background: #f8f9fa;
border-radius: 8px;
}
.detail-item {
display: flex;
justify-content: space-between;
padding: 10px 0;
border-bottom: 1px solid #e8e8e8;
}
.detail-item:last-child {
border-bottom: none;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 15px;
}
.detail-grid .detail-item {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 5px;
border: 1px solid #e8e8e8;
padding: 12px;
border-radius: 6px;
background: white;
}
.detail-item .label {
color: #666;
font-weight: 500;
font-size: 0.9rem;
}
.detail-item .value {
color: #333;
font-weight: 600;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
}
.error-message {
margin: 20px 0;
}
.loading-indicator {
text-align: center;
margin: 20px 0;
}
.loading-text {
margin-top: 10px;
color: #666;
font-size: 0.9rem;
}
.control-card {
border: none;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
}
.controls {
display: flex;
flex-direction: column;
gap: 20px;
}
.control-group h4 {
margin: 0 0 10px 0;
color: #333;
}
.address-input {
margin-bottom: 15px;
}
.control-buttons {
display: flex;
gap: 10px;
}
.instructions-card {
border: none;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
}
.instructions p {
margin: 8px 0;
color: #666;
line-height: 1.6;
}
.instructions code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
font-family: 'Monaco', 'Menlo', 'Courier New', monospace;
font-size: 0.9rem;
}
.footer {
text-align: center;
color: white;
padding: 20px 0;
margin-top: 30px;
opacity: 0.9;
font-size: 0.9rem;
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 10px;
}
.header h1 {
font-size: 2rem;
}
.chain-name-display, .balance-amount {
font-size: 1.5rem;
}
.balance-unit {
font-size: 1.2rem;
}
.address-text {
font-size: 0.9rem;
}
.detail-grid {
grid-template-columns: 1fr;
}
.card-header {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.header-actions {
width: 100%;
justify-content: space-between;
}
}
</style>
Step5:在私有链上发布合约(最简单的helloworld合约)
用到hardhat和solidty
进入准备好的文件夹打开终端
npx hardhat@2.22.10

contracts下删除lock文件
写HelloWorld.sol文件:
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.20;
contract HelloWorld {
function sayHello() public pure returns (string memory) {
return "hello world!!";
}
}
创建scripts文件夹,里面放入deploy.js文件
async function main() {
const HelloWorld = await ethers.getContractFactory("HelloWorld");
const helloContract = await HelloWorld.deploy();
await helloContract.waitForDeployment(); // ✅ 核心修复:替换掉原来的 deployed()
const contractAddress = await helloContract.getAddress(); // ✅ 新增:新版获取合约地址的正确方法
console.log("✅ 合约部署成功(本地私有链地址):", contractAddress);
console.log("✅ 调用合约结果:", await helloContract.sayHello());
}
main().catch(err => { console.error(err); process.exit(1); });
npx hardhat compile编译
npx hardhat run scripts/deploy.js –network hardhat
部署成功后会显示

Step6:创建自己的钱包,用remix部署合约,测试转账功能,测试一个开源的Dapp
用到:metamask,ganache(truffle出的本地节点模拟器)
下载metamask:浏览器扩展商店直接搜索下载,创建自己的账户
下载Ganache:https://archive.trufflesuite.com/ganache/
打开Ganache,Quick启动,会创建10个账户,每个账户包含100eth(也可不快速启动,可以自己设置各种参数)
remix部署合约:
contract文件夹下创建HelloWorld.sol文件:
pragma solidity ^0.8.0;
contract HelloWorld {
string private message;
constructor(string memory initialMessage) {
message = initialMessage;
}
function getMessage() public view returns (string memory) {
return message;
}
function setMessage(string memory newMessage) public {
message = newMessage;
}
}
这是一个最简单的合约,然后compile编译,然后在部署deploy界面环境选择external http provider ,选择Ganache里面的本地地址,输入进入点击确定,可以连接到Ganache账户,里面有部署合约用到的测试币用来消耗Gas
在deploy里输入带双引号的信息,比如“HelloWorld”,即可进行部署,此时可以看到钱包中Eth变为99.xxx,并且Ganache里面也有了交易记录
接下来测试Dapp,宠物商店:https://github.com/OffchainLabs/demo-dapp-pet-shop
git clone下来,然后npm install ,npm run dev跑起来。
接下来连接metamask钱包,即可实现宠物的领养

