2401_87297860头像

区块链实习记录

​
Step1:环境配置

1.下载git,并且配置到环境变量

2.下载npm(nvm进行版本管理,方便后续不同项目需要不同的版本)

3.下载Nodejs

4.下载go环境,并且配置环境变量

5.下载gcc(c语言编译环境,并且配置环境变量)

6.访问h​​​​​​​ttps://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钱包,即可实现宠物的领养

在这里插入图片描述

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/2401_87297860/article/details/156458059

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--