以太坊智能合約開發互動教程:從基礎到實際部署的完整指南

本文提供完整的以太坊智能合約開發互動教程,涵蓋開發環境設定、第一個智能合約、合約部署流程,並提供可直接在瀏覽器中運行的程式碼範例。我們從 Hardhat 開發框架的安裝和配置開始,逐步教學如何編寫、測試和部署智能合約,並展示如何使用 Web3.js 與合約進行互動。同時提供常見錯誤的調試方法和進階合約模式的實際應用。

以太坊智能合約開發互動教程:從基礎到實際部署

目錄

  1. 開發環境設定
  2. 第一個智能合約:簡易儲存合約
  3. 合約部署流程
  4. 互動式程式碼範例
  5. 常見錯誤與調試
  6. 進階合約模式

1. 開發環境設定

安裝必要工具

在进行以太坊智能合约开发之前,需要配置开发环境。以下是推荐的工具组合:

Node.js 環境

# 使用 nvm 安裝 Node.js 18+
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
nvm install 18
nvm use 18
node --version

Hardhat 開發框架

Hardhat 是目前最流行的以太坊开发框架,提供了完整的开发、测试和部署工具链:

# 建立專案目錄
mkdir my-ethereum-dapp
cd my-ethereum-dapp
npm init -y

# 安裝 Hardhat
npm install --save-dev hardhat

# 初始化 Hardhat 專案
npx hardhat init
# 選擇 "Create a JavaScript project"

必要的依賴套件

# 安裝其他必要工具
npm install --save-dev @nomicfoundation/hardhat-toolbox
npm install ethers@^6.11.0
npm install dotenv

本地測試網路

Hardhat 內建了本地区块链网络,可以用于本地测试:

// hardhat.config.js
require("@nomicfoundation/hardhat-toolbox");
require("dotenv").config();

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
  solidity: "0.8.19",
  networks: {
    hardhat: {
      chainId: 31337,
    },
    localhost: {
      url: "http://127.0.0.1:8545",
    },
  },
};

啟動本地区块链网络:

npx hardhat node
# 這會啟動一個本地節點,預設提供 20 個測試帳戶
# 每個帳戶有 10000 ETH 的測試資金

2. 第一個智能合約:簡易儲存合約

合約代碼

以下是一個簡單的 value 儲存合約示範,展示了智能合約的基本結構:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/**
 * @title SimpleStorage
 * @dev 簡易值儲存合約
 * @custom:dev-run-script ./scripts/deploy.js
 */
contract SimpleStorage {
    // 狀態變數
    uint256 private storedValue;
    address public owner;
    
    // 事件
    event ValueChanged(uint256 newValue);
    event OwnerChanged(address oldOwner, address newOwner);
    
    // 修飾符
    modifier onlyOwner() {
        require(msg.sender == owner, "Not the owner");
        _;
    }
    
    // 建構函數
    constructor() {
        owner = msg.sender;
    }
    
    /**
     * @dev 儲存一個值
     * @param _value 要儲存的值
     */
    function store(uint256 _value) public onlyOwner {
        storedValue = _value;
        emit ValueChanged(_value);
    }
    
    /**
     * @dev 讀取儲存的值
     * @return 儲存的值
     */
    function retrieve() public view returns (uint256) {
        return storedValue;
    }
    
    /**
     * @dev 轉移所有權
     * @param newOwner 新的所有者地址
     */
    function transferOwnership(address newOwner) public onlyOwner {
        require(newOwner != address(0), "Invalid address");
        address oldOwner = owner;
        owner = newOwner;
        emit OwnerChanged(oldOwner, newOwner);
    }
}

合約部署腳本

// scripts/deploy.js
const hre = require("hardhat");

async function main() {
  console.log("開始部署 SimpleStorage 合約...");
  
  // 取得部署帳戶
  const [deployer] = await hre.ethers.getSigners();
  console.log("部署帳戶:", deployer.address);
  console.log("帳戶餘額:", (await deployer.provider.getBalance(deployer.address)).toString());
  
  // 部署合約
  const SimpleStorage = await hre.ethers.getContractFactory("SimpleStorage");
  const simpleStorage = await SimpleStorage.deploy();
  
  await simpleStorage.waitForDeployment();
  const contractAddress = await simpleStorage.getAddress();
  
  console.log("SimpleStorage 合約已部署到:", contractAddress);
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

部署合約:

npx hardhat run scripts/deploy.js --network hardhat

3. 合約部署流程

部署到不同網路

部署到本地網路

npx hardhat run scripts/deploy.js --network localhost

部署到 Sepolia 測試網路

首先需要在 .env 檔案中設定私鑰:

# .env
SEPOLIA_RPC_URL=https://sepolia.infura.io/v3/YOUR_INFURA_KEY
PRIVATE_KEY=YOUR_WALLET_PRIVATE_KEY

更新 hardhat.config.js

networks: {
  sepolia: {
    url: process.env.SEPOLIA_RPC_URL,
    accounts: [process.env.PRIVATE_KEY],
  },
},

執行部署:

npx hardhat run scripts/deploy.js --network sepolia

部署到以太坊主網

npx hardhat run scripts/deploy.js --network mainnet

驗證部署的合約

部署完成後,可以在 Etherscan 上驗證合約源代碼:

npx hardhat verify --network sepolia CONTRACT_ADDRESS

4. 互動式程式碼範例

4.1 基本操作互動教學

以下程式碼範例展示了如何與已部署的合約進行互動:

// scripts/interact.js
const hre = require("hardhat");

// 合約 ABI(部分)
const ABI = [
  "function store(uint256 _value) external",
  "function retrieve() external view returns (uint256)",
  "function owner() external view returns (address)"
];

async function main() {
  // 連接到本地網路
  const provider = new hre.ethers.JsonRpcProvider("http://127.0.0.1:8545");
  
  // 使用測試帳戶
  const wallet = hre.ethers.Wallet.fromMnemonic(
    "test test test test test test test test test test test junk"
  ).connect(provider);
  
  // 合約地址(需要替換為實際部署地址)
  const contractAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
  
  // 建立合約實例
  const contract = new hre.ethers.Contract(contractAddress, ABI, wallet);
  
  // 讀取當前值
  console.log("當前儲存值:", await contract.retrieve());
  
  // 儲存新值
  console.log("正在儲存值 42...");
  const tx = await contract.store(42);
  await tx.wait();
  
  // 確認儲存結果
  console.log("儲存後的值:", await contract.retrieve());
}

main();

4.2 自動執行合約

這個範例展示如何設定定時或條件觸發的自動合約操作:

// contracts/AutomatedContract.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

/**
 * @title AutomatedStore
 * @dev 自動化儲存合約,支援條件觸發
 */
contract AutomatedStore is Ownable, ReentrancyGuard {
    uint256 public storedValue;
    uint256 public lastUpdateTime;
    uint256 public updateCount;
    
    // 事件
    event AutoUpdated(uint256 value, uint256 timestamp);
    event ConditionMet(bool condition, uint256 value);
    
    constructor() Ownable(msg.sender) {}
    
    /**
     * @dev 儲存值並記錄時間戳
     */
    function store(uint256 _value) external onlyOwner nonReentrant {
        storedValue = _value;
        lastUpdateTime = block.timestamp;
        updateCount++;
    }
    
    /**
     * @dev 檢查是否滿足特定條件
     * @param threshold 閾值
     * @return 是否滿足條件
     */
    function checkCondition(uint256 threshold) external view returns (bool) {
        bool met = storedValue >= threshold;
        emit ConditionMet(met, storedValue);
        return met;
    }
    
    /**
     * @dev 批量儲存多個值
     * @param values 值陣列
     */
    function batchStore(uint256[] calldata values) external onlyOwner nonReentrant {
        require(values.length <= 100, "Too many values");
        
        for (uint256 i = 0; i < values.length; i++) {
            storedValue = values[i];
            lastUpdateTime = block.timestamp;
            updateCount++;
        }
    }
}

4.3 多重簽名錢包

以下是一個簡化的多重簽名錢包合約範例:

// contracts/MultiSigWallet.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/**
 * @title SimpleMultiSig
 * @dev 簡易多重簽名錢包
 */
contract SimpleMultiSig {
    address[] public owners;
    uint256 public required;
    
    struct Transaction {
        address to;
        uint256 value;
        bytes data;
        bool executed;
        uint256 confirmationCount;
    }
    
    Transaction[] public transactions;
    mapping(uint256 => mapping(address => bool)) public confirmations;
    
    event SubmitTransaction(address indexed owner, uint256 indexed txIndex);
    event ConfirmTransaction(address indexed owner, uint256 indexed txIndex);
    event ExecuteTransaction(address indexed owner, uint256 indexed txIndex);
    
    constructor(address[] memory _owners, uint256 _required) {
        require(_owners.length > 0, "No owners");
        require(_required > 0 && _required <= _owners.length, "Invalid required");
        
        owners = _owners;
        required = _required;
    }
    
    function submitTransaction(address _to, uint256 _value, bytes memory _data) 
        public 
    {
        uint256 txIndex = transactions.length;
        transactions.push(Transaction({
            to: _to,
            value: _value,
            data: _data,
            executed: false,
            confirmationCount: 0
        }));
        
        emit SubmitTransaction(msg.sender, txIndex);
        confirmTransaction(txIndex);
    }
    
    function confirmTransaction(uint256 _txIndex) public {
        require(_txIndex < transactions.length, "Invalid tx");
        require(!confirmations[_txIndex][msg.sender], "Already confirmed");
        
        confirmations[_txIndex][msg.sender] = true;
        transactions[_txIndex].confirmationCount++;
        
        emit ConfirmTransaction(msg.sender, _txIndex);
        
        if (transactions[_txIndex].confirmationCount >= required) {
            executeTransaction(_txIndex);
        }
    }
    
    function executeTransaction(uint256 _txIndex) public {
        Transaction storage tx = transactions[_txIndex];
        require(!tx.executed, "Already executed");
        require(tx.confirmationCount >= required, "Not enough confirmations");
        
        tx.executed = true;
        (bool success, ) = tx.to.call{value: tx.value}(tx.data);
        require(success, "Execution failed");
        
        emit ExecuteTransaction(msg.sender, _txIndex);
    }
    
    receive() external payable {}
}

4.4 前端互動範例(Web3.js)

<!DOCTYPE html>
<html>
<head>
    <title>SimpleStorage 互動範例</title>
    <script src="https://cdn.jsdelivr.net/npm/ethers@6.11.0/dist/ethers.umd.min.js"></script>
    <style>
        body { font-family: Arial, sans-serif; max-width: 600px; margin: 50px auto; padding: 20px; }
        .container { background: #f5f5f5; padding: 20px; border-radius: 8px; }
        input, button { padding: 10px; margin: 5px 0; width: 100%; box-sizing: border-box; }
        button { background: #627eea; color: white; border: none; cursor: pointer; }
        button:hover { background: #4a6bc7; }
        .result { margin-top: 20px; padding: 10px; background: #e8f5e9; border-radius: 4px; }
        .error { background: #ffebee; }
    </style>
</head>
<body>
    <div class="container">
        <h2>SimpleStorage 互動範例</h2>
        
        <button id="connectBtn">連接錢包</button>
        
        <div id="walletInfo" style="display:none;">
            <p>錢包地址: <span id="address"></span></p>
            <p>當前值: <span id="currentValue">載入中...</span></p>
            
            <input type="number" id="valueInput" placeholder="輸入要儲存的值">
            <button id="storeBtn">儲存值</button>
            
            <button id="retrieveBtn">讀取值</button>
        </div>
        
        <div id="result"></div>
    </div>

    <script>
        let contract;
        
        // 合約地址和 ABI
        const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3";
        const CONTRACT_ABI = [
            "function store(uint256 _value) external",
            "function retrieve() external view returns (uint256)",
            "event ValueChanged(uint256 newValue)"
        ];
        
        async function connectWallet() {
            if (typeof window.ethereum !== 'undefined') {
                try {
                    await window.ethereum.request({ method: 'eth_requestAccounts' });
                    const provider = new ethers.BrowserProvider(window.ethereum);
                    const signer = await provider.getSigner();
                    
                    document.getElementById('address').textContent = await signer.getAddress();
                    document.getElementById('walletInfo').style.display = 'block';
                    document.getElementById('connectBtn').textContent = '已連接';
                    
                    contract = new ethers.Contract(CONTRACT_ADDRESS, CONTRACT_ABI, signer);
                    await updateValue();
                } catch (error) {
                    showResult('連接失敗: ' + error.message, true);
                }
            } else {
                showResult('請安裝 MetaMask!', true);
            }
        }
        
        async function storeValue() {
            const value = document.getElementById('valueInput').value;
            if (!value) {
                showResult('請輸入值', true);
                return;
            }
            
            try {
                showResult('交易處理中...', false);
                const tx = await contract.store(value);
                await tx.wait();
                showResult('儲存成功!', false);
                await updateValue();
            } catch (error) {
                showResult('錯誤: ' + error.message, true);
            }
        }
        
        async function retrieveValue() {
            try {
                const value = await contract.retrieve();
                showResult('讀取值: ' + value.toString(), false);
            } catch (error) {
                showResult('錯誤: ' + error.message, true);
            }
        }
        
        async function updateValue() {
            try {
                const value = await contract.retrieve();
                document.getElementById('currentValue').textContent = value.toString();
            } catch (error) {
                document.getElementById('currentValue').textContent = '讀取失敗';
            }
        }
        
        function showResult(message, isError) {
            const resultDiv = document.getElementById('result');
            resultDiv.textContent = message;
            resultDiv.className = 'result' + (isError ? ' error' : '');
        }
        
        document.getElementById('connectBtn').addEventListener('click', connectWallet);
        document.getElementById('storeBtn').addEventListener('click', storeValue);
        document.getElementById('retrieveBtn').addEventListener('click', retrieveValue);
    </script>
</body>
</html>

5. 常見錯誤與調試

5.1 常見編譯錯誤

Solidity 版本衝突

Error: Compiler run failed
ParserError: Source file requires different compiler version

解決方案:確保 hardhat.config.js 中的編譯器版本與合約中指定的版本匹配。

類型錯誤

// 錯誤範例
uint8 public value = 256; // uint8 最大值為 255

// 正確
uint16 public value = 256;

5.2 部署常見問題

Insufficient Balance

Error: insufficient funds for gas * price + value

解決方案:確保帳戶有足夠的 ETH 支付 Gas 費用。在測試網路上可以使用水龍頭獲取測試 ETH。

nonce 錯誤

Error: nonce too low

解決方案:重置帳戶的 nonce 或等待交易確認。

5.3 調試技巧

使用 Hardhat Console

npx hardhat console --network localhost

在 console 中可以直接與合約互動:

const [owner] = await ethers.getSigners();
const SimpleStorage = await ethers.getContractFactory("SimpleStorage");
const contract = await SimpleStorage.deploy();
await contract.waitForDeployment();
await contract.store(100);
console.log(await contract.retrieve());

事件監聽

// 監聽合約事件
contract.on("ValueChanged", (newValue, event) => {
  console.log("值已更改:", newValue.toString());
});

6. 進階合約模式

6.1 可升級合約模式

// contracts/UpgradeableStorage.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/**
 * @title Initializable
 * @dev 可升級合約基礎
 */
abstract contract Initializable {
    bool private _initialized;
    
    modifier initializer() {
        require(!_initialized, "Already initialized");
        _;
        _initialized = true;
    }
}

/**
 * @title UpgradeableStore
 * @dev 可升級的儲存合約
 */
contract UpgradeableStore is Initializable {
    uint256 public value;
    address public owner;
    
    function initialize() public initializer {
        owner = msg.sender;
    }
    
    function setValue(uint256 _value) external {
        require(msg.sender == owner, "Not owner");
        value = _value;
    }
    
    function getValue() external view returns (uint256) {
        return value;
    }
}

6.2 速率限制模式

// contracts/RateLimited.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;

/**
 * @title RateLimited
 * @dev 速率限制合約
 */
abstract contract RateLimited {
    uint256 public rateLimit;
    uint256 public lastUpdate;
    uint256 public available;
    
    constructor(uint256 _rateLimit) {
        rateLimit = _rateLimit;
        lastUpdate = block.timestamp;
        available = _rateLimit;
    }
    
    function _consume(uint256 amount) internal {
        uint256 elapsed = block.timestamp - lastUpdate;
        available += elapsed * (rateLimit / 3600);
        if (available > rateLimit) available = rateLimit;
        
        require(available >= amount, "Rate limit exceeded");
        available -= amount;
        lastUpdate = block.timestamp;
    }
}

總結

本教學涵蓋了以太坊智能合約開發的核心知識,從環境設定、合約編寫、部署流程到實際互動操作。透過這些範例,讀者應該能夠:

  1. 正確配置 Hardhat 開發環境
  2. 編寫和部署基本的智能合約
  3. 使用 JavaScript 與合約進行互動
  4. 理解常見錯誤並掌握調試技巧
  5. 應用進階合約模式解決實際問題

建議讀者實際動手操作這些範例,體驗以太坊智能合約開發的完整流程。

延伸閱讀與來源

這篇文章對您有幫助嗎?

評論

發表評論

注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。

目前尚無評論,成為第一個發表評論的人吧!