Solidity 程式碼編譯練習完整指南:從線上工具到本地開發環境
本文提供一個完整的 Solidity 程式碼編譯練習指南,涵蓋從線上 IDE 到本地開發環境的多種練習方式。從最基礎的 Hello World 合約開始,逐步過渡到 ERC-20 代幣合約、去中心化投票系統等實際應用。包含完整的程式碼範例、詳細的語法解釋、以及使用 Remix IDE 和 Hardhat 的實際操作步驟。
Solidity 程式碼編譯練習完整指南:從線上工具到本地開發環境
概述
學習以太坊智慧合約開發的最佳方式是實際動手編寫和部署程式碼。本文提供一個完整的 Solidity 程式碼編譯練習指南,涵蓋從線上 IDE 到本地開發環境的多種練習方式。我們將帶領讀者通過一系列由淺入深的練習專案,逐步掌握 Solidity 語言的核心概念和開發技巧。
Solidity 是以太坊智慧合約開發的主要程式語言,目前版本已經發展到 0.8.x 系列。作為一種靜態類型語言,Solidity 借鑒了 JavaScript、Python 和 C++ 的語法特性,同時針對區塊鏈環境進行了大量優化,例如原生的地址類型、事件日誌機制、以及ether 和 gas 相關的特殊語法。
本文適合完全沒有程式設計經驗的初學者,以及希望系統性提升智慧合約開發技能的進階讀者。我們的練習專案將從最基礎的「Hello World」合約開始,逐步過渡到具有實際應用價值的 ERC-20 代幣合約、去中心化投票系統、以及簡單的借貸合約。每個練習都包含完整的程式碼範例、詳細的語法解釋、以及實際操作的步驟說明。
通過完成本文的所有練習,讀者將能夠:理解 Solidity 語言的基礎語法和類型系統、掌握智慧合約的開發調試流程、能夠獨立編寫和部署基本的智慧合約、了解智慧合約的安全性考量,並具備進一步學習進階主題的基礎。
第一部分:線上 IDE 快速上手
Remix IDE 介紹
Remix IDE 是以太坊官方推薦的智慧合約線上開發環境,無需安裝任何軟體,通過瀏覽器即可使用。它提供了完整的智慧合約開發工具鏈,包括:程式碼編輯器、編譯器、部署工具、調試器、以及測試框架。
訪問 remix.ethereum.org 即可開始使用 Remix IDE。頁面載入後,你會看到一個預設的 Solidity 檔案「1_Storage.sol」,這是一個簡單的存儲合約範例。我們將從這個範例開始,逐步學習 Remix IDE 的使用方法。
Remix IDE 的介面主要分為四個區域:左側是檔案瀏覽器,用於管理專案中的多個檔案;中間是程式碼編輯器,用於編寫和查看程式碼;左下角是編譯控制面板,用於編譯合約;右下角是部署和運行面板,用於將合約部署到區塊鏈或本地模擬器。
第一個練習:Hello World 合約
讓我們從最基本的練習開始,創建一個簡單的「Hello World」智慧合約。
在 Remix IDE 中,點擊左上角的「New File」按鈕,創建一個名為「HelloWorld.sol」的新檔案。然後,輸入以下程式碼:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract HelloWorld {
// 存儲訊息的變量
string private message;
// 事件:用於記錄訊息變更
event MessageChanged(string oldMessage, string newMessage);
// 建構函數:合約部署時執行
constructor() {
message = "Hello, Ethereum!";
}
// 獲取訊息的函數
function getMessage() public view returns (string memory) {
return message;
}
// 設定訊息的函數
function setMessage(string calldata _message) public {
string memory oldMessage = message;
message = _message;
emit MessageChanged(oldMessage, _message);
}
}
讓我們解讀這段程式碼的各個部分:
版權聲明和編譯器版本
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
第一行是 SPDX 版權標識,說明合約使用 MIT 許可證。第二行指定編譯器版本,「^0.8.19」表示接受 0.8.19 或更高版本的 Solidity 編譯器(但在 0.9.0 之前)。
合約定義
contract HelloWorld { ... }
Solidity 中使用 contract 關鍵字定義智慧合約,類似於其他語言中的 class(類)。
狀態變量
string private message;
狀態變量是永久存儲在區塊鏈上的資料。private 關鍵字表示該變量只能從合約內部訪問(儘管區塊鏈上的任何人都可以看到這些資料,只是不能直接讀取)。
事件
event MessageChanged(string oldMessage, string newMessage);
事件是用於記錄區塊鏈上發生的事情的機制。客戶端可以「監聽」事件來獲取合約狀態的變化通知。
建構函數
constructor() { ... }
建構函數在合約部署時執行一次,用於初始化合約的初始狀態。
函數
Solidity 中的函數可以分為兩類:view 函數(只讀,不會修改區塊鏈狀態)和非 view 函數(會修改狀態,需要支付 Gas)。
編譯合約
現在讓我們編譯剛才編寫的合約:
- 點擊左側菜單中的「Solidity Compiler」圖示(看起來像個向下箭頭)
- 確認編譯器版本選擇為 0.8.19(或你使用的版本)
- 點擊「Compile HelloWorld.sol」按鈕
- 觀察編譯結果:如果成功,會顯示綠色的勾選標記和「Compilation successful」訊息;如果有錯誤,會顯示具體的錯誤訊息和行號
常見的編譯錯誤包括:語法錯誤(拼寫錯誤、缺少分號)、類型錯誤(類型不匹配的賦值)、以及版本兼容性問題。
部署合約
編譯成功後,讓我們將合約部署到區塊鏈上進行測試:
- 點擊左側菜單中的「Deploy & Run Transactions」圖示
- 在「Environment」下拉選單中,選擇「Injected Provider - MetaMask」
- 如果尚未連接 MetaMask,會彈出連接請求,點擊「連接」
- 確認 MetaMask 中選擇的是 Sepolia 測試網路
- 點擊「Deploy」按鈕
- 在 MetaMask 中確認交易,點擊「確認」
部署完成後,你會在「Deployed Contracts」區域看到新部署的合約。點擊合約名稱可以展開所有可呼叫的函數。
與合約互動
現在讓我們嘗試呼叫合約的函數:
- 點擊「getMessage」函數旁的橙色的「Call」按鈕
- 觀察下方的終端輸出,應該顯示返回值「Hello, Ethereum!」(這是我們在建構函數中設定的初始值)
現在讓我們修改訊息:
- 在「setMessage」函數的輸入框中,輸入新的訊息,例如「Hello, Web3!」
- 點擊藍色的「Transact」按鈕
- MetaMask 會彈出交易確認視窗,顯示交易詳情:
- 交易類型:合約部署
- Gas 估算:大約 0.001 ETH
- 點擊「確認」提交交易
- 等待交易被區塊確認(大約 10-20 秒)
- 再次呼叫「getMessage」函數,確認訊息已經更新
這個練習展示了智慧合約的完整生命週期:編寫、編譯、部署、以及互動。
第二部分:Solidity 基礎語法練習
資料類型練習
Solidity 提供了豐富的資料類型。讓我們通過練習來掌握這些類型的使用:
整數類型
Solidity 支持有符號和無符號整數,寬度從 8 位元到 256 位元:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract DataTypesPractice {
// 無符號整數
uint8 public smallUint = 255; // 0 到 255
uint16 public mediumUint = 65535; // 0 到 65535
uint256 public largeUint = 1000; // 0 到 2^256-1
// 有符號整數
int8 public smallInt = -128; // -128 到 127
int256 public largeInt = -1000; // -2^255 到 2^255-1
// 地址類型
address public owner = msg.sender; // 160 位元地址
// 位元組類型
bytes32 public data = "Hello"; // 固定長度位元組陣列
// 布林類型
bool public flag = true;
// 枚舉類型
enum Status { Pending, Active, Completed, Cancelled }
Status public currentStatus = Status.Pending;
// 陣列
uint[] public numbers = [1, 2, 3, 4, 5];
// 結構體
struct Person {
string name;
uint age;
address wallet;
}
Person[] public people;
// 新增人員
function addPerson(string memory _name, uint _age) public {
people.push(Person(_name, _age, msg.sender));
}
// 獲取人員數量
function getPeopleCount() public view returns (uint) {
return people.length;
}
}
練習要求:
- 部署這個合約
- 呼叫
addPerson函數添加幾個人員 - 呼叫
getPeopleCount確認人員已添加 - 嘗試將
smallUint設定為 256,觀察會發生什麼錯誤(會發生溢位錯誤)
控制流練習
Solidity 的控制流語法與其他 C 系列語言類似:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract ControlFlowPractice {
uint[] public numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// 條件判斷
function getResultIfElse(uint _input) public pure returns (string memory) {
if (_input > 100) {
return "大於 100";
} else if (_input > 50) {
return "大於 50 但小於等於 100";
} else {
return "小於等於 50";
}
}
// For 迴圈:計算總和
function getSum() public view returns (uint) {
uint sum = 0;
for (uint i = 0; i < numbers.length; i++) {
sum += numbers[i];
}
return sum;
}
// While 迴圈:找最大值
function getMax() public view returns (uint) {
uint max = numbers[0];
uint i = 1;
while (i < numbers.length) {
if (numbers[i] > max) {
max = numbers[i];
}
i++;
}
return max;
}
// 迴圈中的 break 和 continue
function findFirstEven() public view returns (uint) {
for (uint i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 == 0) {
return numbers[i]; // 找到第一個偶數就返回
}
}
return 0; // 沒有找到偶數
}
}
函數可見性和修飾符練習
Solidity 中的函數有不同的可見性和特殊修飾符:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract FunctionModifiersPractice {
address public owner;
uint public counter = 0;
bool public paused = false;
constructor() {
owner = msg.sender;
}
// 函數修飾符:檢查是否是 owner
modifier onlyOwner() {
require(msg.sender == owner, "Only owner can call this");
_;
}
// 函數修飾符:檢查是否暫停
modifier whenNotPaused() {
require(!paused, "Contract is paused");
_;
}
// 公開函數:任何人都可以調用
function incrementPublic() public whenNotPaused {
counter++;
}
// 外部函數:只能從合約外部調用
function incrementExternal() external whenNotPaused {
counter++;
}
// 內部函數:只能在合約內部調用
function _internalHelper() internal view returns (address) {
return owner;
}
// 私有函數:只能在定義它的合約內部調用
function _privateHelper() private pure returns (bool) {
return true;
}
// 只能從 owner 調用的函數
function setPaused(bool _paused) public onlyOwner {
paused = _paused;
}
// view 函數:承諾不修改狀態
function getOwner() public view returns (address) {
return owner;
}
// pure 函數:承諾既不讀取也不修改狀態
function add(uint a, uint b) public pure returns (uint) {
return a + b;
}
// payable 函數:可以接收以太幣
function deposit() public payable {}
function getBalance() public view returns (uint) {
return address(this).balance;
}
}
第三部分:ERC-20 代幣合約練習
什麼是 ERC-20
ERC-20 是以太坊上代幣(Token)的標準介面規範。遵循這個標準的代幣可以被錢包、交易所和其他應用程式統一識別和管理。截至 2026 年第一季度,已有數十萬種 ERC-20 代幣在以太坊區塊鏈上發行。
ERC-20 標準定義了以下必需的功能:
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
練習:實現完整的 ERC-20 代幣
讓我們通過練習來實現一個完整的 ERC-20 代幣合約:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract MyToken {
// 代幣名稱
string public constant name = "MyToken";
// 代幣符號
string public constant symbol = "MTK";
// 小數位數(精度)
uint8 public constant decimals = 18;
// 總供應量
uint256 private _totalSupply;
// 餘額映射:地址 -> 餘額
mapping(address => uint256) private _balances;
// 授權映射:所有者 -> 被授權者 -> 授權數量
mapping(address => mapping(address => uint256)) private _allowances;
// 事件
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
// 建構函數:鑄造初始代幣
constructor(uint256 initialSupply) {
_totalSupply = initialSupply * 10 ** decimals;
_balances[msg.sender] = _totalSupply;
emit Transfer(address(0), msg.sender, _totalSupply);
}
// 獲取總供應量
function totalSupply() public view returns (uint256) {
return _totalSupply;
}
// 獲取帳戶餘額
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
// 轉帳
function transfer(address to, uint256 amount) public returns (bool) {
require(to != address(0), "Transfer to zero address");
require(_balances[msg.sender] >= amount, "Insufficient balance");
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
// 獲取授權額度
function allowance(address owner, address spender) public view returns (uint256) {
return _allowances[owner][spender];
}
// 授權
function approve(address spender, uint256 amount) public returns (bool) {
require(spender != address(0), "Approve to zero address");
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
// 從其他帳戶轉帳(需要預先授權)
function transferFrom(address from, address to, uint256 amount) public returns (bool) {
require(to != address(0), "Transfer to zero address");
require(_balances[from] >= amount, "Insufficient balance");
require(_allowances[from][msg.sender] >= amount, "Allowance exceeded");
_balances[from] -= amount;
_allowances[from][msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
return true;
}
// 增加授權額度
function increaseAllowance(address spender, uint256 addedValue) public returns (bool) {
require(spender != address(0), "Increase allowance to zero address");
_allowances[msg.sender][spender] += addedValue;
emit Approval(msg.sender, spender, _allowances[msg.sender][spender]);
return true;
}
// 減少授權額度
function decreaseAllowance(address spender, uint256 subtractedValue) public returns (bool) {
require(spender != address(0), "Decrease allowance to zero address");
require(_allowances[msg.sender][spender] >= subtractedValue, "Below zero");
_allowances[msg.sender][spender] -= subtractedValue;
emit Approval(msg.sender, spender, _allowances[msg.sender][spender]);
return true;
}
}
練習步驟:
- 將這段程式碼複製到 Remix IDE 中
- 在部署時,輸入初始供應量(例如 1000)
- 部署後,你會獲得全部的代幣
- 嘗試轉帳一部分代幣到另一個地址
- 嘗試使用
approve和transferFrom進行授權轉帳
OpenZeppelin ERC-20 合約
在實際生產環境中,通常不會從頭編寫 ERC-20 合約,而是使用經過審計的 OpenZeppelin 庫:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract MyToken is ERC20 {
constructor(uint256 initialSupply) ERC20("MyToken", "MTK") {
_mint(msg.sender, initialSupply * 10 ** decimals());
}
}
使用 OpenZeppelin 的優勢包括:經過社區審計的安全程式碼、完整的功能實現、以及易於擴展(可以添加鑄造、銷毀等功能)。
第四部分:本地開發環境練習
Hardhat 環境搭建
對於更複雜的專案,建議使用 Hardhat 作為本地開發環境。Hardhat 提供了更強大的功能和靈活性:
- 首先,確保已安裝 Node.js(版本 16 或以上)
- 創建新項目資料夾並初始化:
mkdir my-token-project
cd my-token-project
npm init -y
npm install --save-dev hardhat
npx hardhat init
- 選擇「Create a JavaScript project」創建項目
- 項目結構如下:
my-token-project/
├── contracts/ # 智慧合約原始碼
├── scripts/ # 部署腳本
├── test/ # 測試檔案
├── hardhat.config.js # Hardhat 配置
└── package.json # 專案依賴
部署腳本練習
創建一個部署腳本來部署 ERC-20 代幣合約:
// scripts/deploy.js
const hre = require("hardhat");
async function main() {
console.log("部署 ERC-20 代幣合約...");
// 獲取合約工廠
const MyToken = await hre.ethers.getContractFactory("MyToken");
// 部署合約,傳入初始供應量參數
const token = await MyToken.deploy(1000000);
// 等待部署完成
await token.deployed();
// 輸出部署後的合約地址
console.log("代幣合約已部署到:", token.address);
// 獲取部署者地址
const [deployer] = await hre.ethers.getSigners();
console.log("部署者地址:", deployer.address);
// 檢查部署者的餘額
const balance = await token.balanceOf(deployer.address);
console.log("部署者餘額:", hre.ethers.utils.formatEther(balance), "MTK");
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
測試腳本練習
為代幣合約編寫測試:
// test/MyToken.js
const { expect } = require("chai");
const { ethers } = require("hardhat");
describe("MyToken", function() {
let token;
let owner;
let addr1;
let addr2;
// 在每個測試案例前部署合約
beforeEach(async function() {
const MyToken = await ethers.getContractFactory("MyToken");
[owner, addr1, addr2] = await ethers.getSigners();
token = await MyToken.deploy(1000);
await token.deployed();
});
// 測試代幣名稱和符號
it("should have correct name and symbol", async function() {
expect(await token.name()).to.equal("MyToken");
expect(await token.symbol()).to.equal("MTK");
});
// 測試初始供應量
it("should assign total supply to owner", async function() {
const ownerBalance = await token.balanceOf(owner.address);
expect(await token.totalSupply()).to.equal(ownerBalance);
});
// 測試轉帳功能
it("should transfer tokens between accounts", async function() {
// 從 owner 轉帳 50 個代幣到 addr1
await token.transfer(addr1.address, 50);
expect(await token.balanceOf(addr1.address)).to.equal(50);
// addr1 轉帳 20 個代幣到 addr2
await token.connect(addr1).transfer(addr2.address, 20);
expect(await token.balanceOf(addr2.address)).to.equal(20);
expect(await token.balanceOf(addr1.address)).to.equal(30);
});
// 測試餘額不足的情況
it("should fail when sender doesn't have enough tokens", async function() {
const initialOwnerBalance = await token.balanceOf(owner.address);
await expect(
token.connect(addr1).transfer(owner.address, 1)
).to.be.revertedWith("Insufficient balance");
expect(await token.balanceOf(owner.address)).to.equal(initialOwnerBalance);
});
// 測試授權和轉帳
it("should update allowances", async function() {
// owner 授權 addr1 使用 100 個代幣
await token.approve(addr1.address, 100);
expect(await token.allowance(owner.address, addr1.address)).to.equal(100);
// addr1 使用授權轉帳
await token.connect(addr1).transferFrom(owner.address, addr2.address, 50);
expect(await token.balanceOf(addr2.address)).to.equal(50);
expect(await token.allowance(owner.address, addr1.address)).to.equal(50);
});
});
運行測試:
npx hardhat test
如果所有測試通過,你應該能看到類似的輸出:
MyToken
✓ should have correct name and symbol
✓ should assign total supply to owner
✓ should transfer tokens between accounts
✓ should fail when sender doesn't have enough tokens
✓ should update allowances
5 passing (2s)
第五部分:常見錯誤與調試技巧
常見編譯錯誤
語法錯誤
// 錯誤:忘記分號
uint256 public myVar = 42
// 正確
uint256 public myVar = 42;
類型錯誤
// 錯誤:嘗試將 address 賦值給 uint256
uint256 public myUint = msg.sender;
// 正確
address public myAddress = msg.sender;
可見性錯誤
// 錯誤:狀態變量必須指定可見性
uint256 publicBalance = 100;
// 正確
uint256 public publicBalance = 100;
常見運行時錯誤
Require 失敗
// 這個交易會失敗並回滾
function testRequire(uint256 _input) public pure {
require(_input > 10, "Input must be greater than 10");
}
陣列越界
function getArrayValue(uint256 index) public view returns (uint256) {
return myArray[index]; // 如果 index >= myArray.length,會失敗
}
整數溢位
function testOverflow() public pure returns (uint8) {
uint8 public a = 255;
a += 1; // 會導致溢位錯誤(在 Solidity 0.8+ 中會自動檢查)
}
Remix 調試工具使用
Remix IDE 提供了強大的調試工具:
- 錯誤定位:點擊編譯錯誤訊息,可以直接跳轉到出錯的行號
- 交易調試:在「Deploy & Run Transactions」面板中,點擊已完成的交易,可以看到詳細的執行過程
- 日誌輸出:使用
console.log(需要引入 Hardhat 的 console.sol)可以在調試時輸出變數值
結論
通過本文的練習,我們從最基本的「Hello World」合約開始,逐步深入到 ERC-20 代幣合約的實現,以及使用 Hardhat 進行專業級的開發和測試。這些練習涵蓋了 Solidity 開發的核心知識點,包括:
- 基礎語法:資料類型、變量、函數
- 控制流:條件判斷、迴圈
- 面向對象特性:繼承、修飾符
- 標準實現:ERC-20 代幣
- 開發工具:Remix IDE、Hardhat
建議讀者在完成這些練習後,嘗試擴展這些專案,例如:為代幣添加鑄造和銷毀功能、實現投票合約、或創建簡單的拍賣系統。這些擴展練習將幫助你進一步鞏固所學知識,並為開發更複雜的智慧合約打下堅實的基礎。
延伸閱讀
- Solidity 官方文檔:docs.soliditylang.org
- OpenZeppelin 合約庫:docs.openzeppelin.com/contracts
- Hardhat 文檔:hardhat.org/docs
- Remix IDE 文檔:remix-ide.readthedocs.io
- 以太坊開發者資源:ethereum.org/developers
相關文章推薦
相關文章
- 以太坊智能合約完全新手入門:從概念到第一個合約 — 本文專為完全新手設計,用最簡單的語言解釋智能合約的概念,並手把手教學創建第一個智能合約。文章涵蓋智能合約的起源與原理、Solidity 程式語言基礎語法、簡易代幣合約實作、以及部署合約的完整步驟。同時介紹智能合約的安全考量,幫助讀者建立正確的安全意識。
- 以太坊新手入門手冊:從零開始理解區塊鏈與以太坊 — 專為完全不了解區塊鏈技術的讀者設計的入門手冊。用最簡單的語言解釋區塊鏈、以太坊、ETH、智慧合約等核心概念,並提供創建錢包、購買 ETH、安全保護等實用指南。
- 以太坊跨鏈橋接完整實作教學:從基礎概念到 MetaMask 操作指南 — 本文深入介紹區塊鏈橋接技術的運作原理,包含鎖定與鑄造、流動性網路、驗證者機制等核心概念,並提供詳細的 MetaMask 跨鏈橋接操作步驟。我們涵蓋 Stargate、Hop Protocol、Across Protocol 等主流橋接協議的比較分析,以及 Arbitrum、Optimism、Polygon 等 Layer 2 網路的官方橋接教學,同時提供完整的風險管理策略與最佳實踐建議。
- 智慧合約基礎概念:從原理到實踐 — 智慧合約基礎概念詳解,從原理到實踐的完整教學,涵蓋 EVM、Solidity 與智能合約開發基礎。
- 以太坊學習路徑完整指南:從新手到專業開發者的系統化旅程 — 本文提供一條完整的以太坊學習路徑,從基礎概念到進階開發,配合可執行的程式碼範例和即時鏈上數據分析,幫助讀者系統性地掌握以太坊技術。內容涵蓋區塊鏈基礎、以太坊核心概念、EVM 與 Gas 機制、智慧合約開發、DeFi 協議實戰、Layer 2 擴容方案、帳戶抽象、零知識證明等主題。每個階段都包含具體的操作範例,讀者可以在實際環境中運行這些程式碼,從而加深對概念的理解。同時引用最新的鏈上數據,幫助讀者理解以太坊網路的實際運行狀態。這是新手入門以太坊開發的最佳指南。
延伸閱讀與來源
- Ethereum.org 以太坊官方入口
- EthHub 以太坊知識庫
這篇文章對您有幫助嗎?
請告訴我們如何改進:
評論
發表評論
注意:由於這是靜態網站,您的評論將儲存在本地瀏覽器中,不會公開顯示。
目前尚無評論,成為第一個發表評論的人吧!