开发以太坊合约教程
以太坊智能合约是运行在以太坊区块链上的代码,它们允许开发者构建去中心化应用程序(DApps)。本教程将引导你了解如何使用Solidity编写、编译、部署和测试简单的以太坊智能合约。
准备工作
在开始开发以太坊 DApp 之前,你需要配置必要的开发环境,确保安装以下工具:
-
Node.js 和 npm (或 yarn):
Node.js 是一个 JavaScript 运行时环境,而 npm(Node Package Manager)或 yarn 是用于管理 JavaScript 项目依赖的包管理器。它们对于安装和管理 DApp 开发所需的各种库和工具至关重要。 请访问
Node.js 官方网站
下载并安装适合你操作系统的版本。 安装 Node.js 通常会自动包含 npm。 如果你更喜欢使用 yarn,可以在安装 Node.js 后使用 npm 安装 yarn:
npm install -g yarn
。 -
Truffle Suite:
Truffle Suite 是一个全面的以太坊开发框架,极大地简化了智能合约的开发、编译、部署和测试流程。 它包含 Truffle、Ganache 和 Drizzle 三个核心组件。 使用 npm 全局安装 Truffle:
npm install -g truffle
安装完成后,可以通过运行
truffle version
命令来验证 Truffle 是否成功安装。 -
Ganache:
Ganache 提供了一个快速、简单且可配置的个人以太坊区块链,非常适合本地 DApp 开发和测试。 它模拟了真实的以太坊环境,允许你在隔离的环境中安全地测试智能合约,而无需消耗真实的网络 Gas 费用。
你可以通过多种方式启动 Ganache:
-
Truffle Develop:
这是 Truffle 集成的一个轻量级开发环境。 在项目根目录下,运行
truffle develop
命令,然后在 Truffle Develop 控制台中输入ganache
来启动 Ganache 实例。 -
Ganache CLI:
一个命令行版本的 Ganache,可以通过 npm 安装:
npm install -g ganache-cli
。 然后,你可以使用ganache-cli
命令启动它,并可以自定义各种选项,例如端口号和区块链 ID。 - Ganache UI: 一个图形用户界面版本的 Ganache,提供了更直观的交互方式。 你可以从 Truffle Suite 官方网站 下载并安装独立的 Ganache 应用程序。
无论你选择哪种方式,确保 Ganache 正在运行,并且你知道它的 RPC 服务器地址(通常是
http://127.0.0.1:7545
或http://localhost:7545
)。 -
Truffle Develop:
这是 Truffle 集成的一个轻量级开发环境。 在项目根目录下,运行
-
MetaMask:
MetaMask 是一个流行的浏览器扩展程序,充当以太坊钱包,允许你与 DApp 交互。 它安全地存储你的以太坊账户密钥,并允许你签署交易和授权 DApp 访问你的账户。
访问 MetaMask 官方网站 下载并安装适用于你浏览器的 MetaMask 扩展。 安装完成后,按照屏幕上的说明创建一个新的 MetaMask 钱包或导入现有的钱包。 确保将你的助记词(也称为种子短语)安全地存储在离线位置,因为这是恢复你的钱包的唯一方法。
配置 MetaMask 连接到你的 Ganache 实例。 在 MetaMask 中,单击网络选择器(默认情况下显示 "Ethereum Mainnet"),然后选择 "Custom RPC"。 输入 Ganache 的 RPC 服务器地址(例如
http://127.0.0.1:7545
)作为新的网络 URL,并为网络指定一个名称(例如 "Ganache")。 保存设置后,MetaMask 将连接到你的本地 Ganache 区块链。
创建项目
要开始使用 Truffle 构建以太坊智能合约,第一步是创建一个新的 Truffle 项目。Truffle 提供了一个便捷的命令来初始化项目结构,省去了手动创建目录和配置文件的麻烦。
使用以下命令创建项目并进入项目目录:
mkdir eth-contract-tutorial
cd eth-contract-tutorial
truffle init
mkdir eth-contract-tutorial
命令用于创建一个名为 "eth-contract-tutorial" 的新目录,这将作为你的智能合约项目的根目录。
cd eth-contract-tutorial
命令用于将当前工作目录更改为新创建的 "eth-contract-tutorial" 目录。
truffle init
命令是 Truffle 框架提供的初始化命令,它会在当前目录下创建一个基本的项目结构,为后续的智能合约开发奠定基础。
执行
truffle init
命令后,Truffle 会自动生成以下关键目录和文件,构成项目的核心结构:
-
contracts/
: 这个目录用于存放你的 Solidity 合约源代码文件。每个智能合约都应该保存在这个目录下,以便 Truffle 能够找到并编译它们。 Solidity 是一种专门为编写智能合约而设计的编程语言,类似于 JavaScript 或 Python,但具有以太坊虚拟机(EVM)特定的语法和特性。 -
migrations/
: 这个目录用于存放部署脚本。部署脚本是一系列 JavaScript 文件,其中包含将你的智能合约部署到以太坊区块链上的指令。Truffle 使用这些脚本来自动化部署过程,确保合约按照正确的顺序和配置部署。每个部署脚本都应该以数字编号开头,例如1_initial_migration.js
。 -
test/
: 这个目录用于存放 JavaScript 测试文件。测试文件用于验证你的智能合约的功能是否符合预期。通过编写测试用例,你可以确保合约的逻辑正确、安全可靠。Truffle 集成了 Mocha 和 Chai 等流行的 JavaScript 测试框架,可以方便地编写和运行测试。 -
truffle-config.js
: 这个文件是 Truffle 的配置文件,用于指定项目的各种设置,例如编译器版本、网络配置、合约部署地址等。你可以在这个文件中配置 Truffle 的行为,以适应不同的开发环境和部署需求。 例如,可以配置连接到 Ganache 本地测试网络,或者连接到 Ropsten 或 Mainnet 等以太坊公共网络。
编写智能合约
在
contracts/
目录下创建一个名为
SimpleStorage.sol
的文件,并添加以下 Solidity 代码。 此文件将包含我们智能合约的定义。
Solidity 代码:
pragma solidity ^0.8.0;
contract SimpleStorage {
uint256 private storedData;
constructor(uint256 initialValue) {
storedData = initialValue;
}
function set(uint256 x) public {
storedData = x;
}
function get() public view returns (uint256) {
return storedData;
}
}
该合约是一个简单的存储合约,允许用户存储一个
uint256
(256位无符号整数) 类型的数据,并提供
set
函数来修改数据以及
get
函数来读取存储的数据。
-
pragma solidity ^0.8.0;
: 声明 Solidity 编译器的版本。 该声明指定了编译合约所需的编译器版本。 使用^0.8.0
表示编译器版本必须大于等于 0.8.0,但小于 0.9.0,确保兼容性并避免使用过时的编译器特性。 -
contract SimpleStorage { ... }
: 定义一个名为SimpleStorage
的合约。 合约是 Solidity 中智能合约的基本构建块,类似于面向对象编程中的类。 它包含了状态变量和函数,定义了合约的行为。 -
uint256 private storedData;
: 声明一个私有的uint256
类型的状态变量storedData
,用于存储数据。uint256
是 Solidity 中常用的无符号整数类型,可以存储非常大的正整数。private
关键字表示该变量只能在合约内部访问,不能从合约外部直接访问。 -
constructor(uint256 initialValue) { ... }
: 定义一个构造函数,在合约部署到区块链时执行,并且只执行一次。构造函数的目的是初始化合约的状态变量。 在此例中,构造函数接收一个uint256
类型的参数initialValue
,并将其赋值给storedData
,从而初始化存储的数据。 -
function set(uint256 x) public { ... }
: 定义一个公共函数set
,用于设置storedData
的值。public
关键字表示该函数可以从合约外部调用。 该函数接收一个uint256
类型的参数x
,并将其赋值给storedData
,从而更新存储的数据。 任何用户或合约都可以调用此函数来更改存储的值。 -
function get() public view returns (uint256) { ... }
: 定义一个公共的只读函数get
,用于获取storedData
的值。public
关键字表示该函数可以从合约外部调用。view
关键字表示该函数不会修改区块链的状态。returns (uint256)
表示该函数返回一个uint256
类型的值,即storedData
的值。 由于get
函数是只读的,因此调用它不需要消耗 Gas,因为它不会更改任何链上数据。
编写部署脚本
在
migrations/
目录下创建一个名为
1_deploy_simple_storage.js
的 JavaScript 文件。该文件将包含部署
SimpleStorage
合约所需的代码。确保文件命名符合Truffle的迁移约定,以便Truffle能够正确识别和执行该脚本。通常,文件名以数字开头,表示迁移的执行顺序。
javascript
const SimpleStorage = artifacts.require("SimpleStorage");
上述代码首先使用
artifacts.require("SimpleStorage")
加载
SimpleStorage
合约的工件(artifact)。工件包含了合约的ABI(应用程序二进制接口)和字节码,Truffle 通过这些信息与合约进行交互和部署。
artifacts
对象是 Truffle 提供的一个全局对象,用于访问已编译的合约。
javascript
module.exports = function (deployer) {
deployer.deploy(SimpleStorage, 42);
};
这段代码定义了一个 Truffle 迁移函数。该函数接收一个
deployer
对象作为参数,
deployer
对象用于执行合约的部署。
deployer.deploy(SimpleStorage, 42)
指示 Truffle 部署
SimpleStorage
合约,并将初始值
42
作为构造函数的参数传递给它。 这意味着,当合约被部署到区块链上时,其内部存储的某个变量(通常是名为
storedData
的变量)将被初始化为
42
。部署脚本是连接Solidity智能合约和Truffle部署流程的桥梁。
编译合约
在项目根目录下,执行以下命令以编译Solidity智能合约:
truffle compile
truffle compile
命令会调用Solidity编译器(通常是
solc
),处理项目中的所有
.sol
文件。针对
SimpleStorage.sol
合约,编译过程会产生两个关键的输出文件,存放在
build/contracts/
目录下。
生成的是合约的应用二进制接口(Application Binary Interface),简称ABI。ABI是一个JSON格式的文件,它精确地描述了合约的所有公共接口,包括函数签名、参数类型、返回值类型以及事件定义。ABI是连接区块链世界和外部应用程序的桥梁,允许开发者通过编程方式调用合约函数,并监听合约发出的事件。例如,JavaScript库
web3.js
或
ethers.js
需要ABI来编码和解码与合约交互的数据,从而实现与智能合约的无缝集成。
编译过程还会生成合约的字节码(bytecode)。字节码是合约的低级可执行代码,以十六进制字符串形式存在。它是合约的实际逻辑在以太坊虚拟机(EVM)上运行的指令集。当我们将合约部署到区块链上时,实际上是将这份字节码上传到区块链网络。矿工验证交易并将其包含在区块中后,EVM将执行这些字节码,实现合约定义的各种功能。字节码的安全性至关重要,因为任何漏洞都可能导致资金损失或其他严重后果。
配置 Truffle
打开
truffle-config.js
文件,这是 Truffle 项目的核心配置文件,用于定义项目设置,包括网络配置、编译器版本、合约目录等。请务必仔细检查并确保配置了正确的网络设置。默认情况下,Truffle 已经配置了
development
网络,它预设为连接到 Ganache 个人区块链,方便本地开发和测试。
如果你想连接到其他的以太坊网络,例如 Ropsten、Goerli 等测试网络或以太坊主网络,你需要配置相应的网络设置。这包括关键参数如
host
(节点主机地址)、
port
(节点端口号)、
network_id
(网络 ID,用于区分不同的以太坊网络) 和
gas
(Gas 限制,用于设置交易执行的最大 Gas 消耗量)。为了进行合约部署和发送交易,你还需要提供一个拥有足够余额的以太坊账户的私钥或助记词。请务必妥善保管私钥和助记词,避免泄露,以防资产损失。 可以通过 Infura、Alchemy 等节点服务商提供的 URL 来连接到不同的以太坊网络, 并使用 HDWalletProvider 等工具来管理账户私钥。
一个连接到 Ganache 的网络配置示例如下。 该配置定义了一个名为 "development" 的网络,连接到本地 Ganache 实例。
host
设置为
127.0.0.1
,表示本地主机。
port
设置为
7545
,这是 Ganache 默认的 RPC 端口。
network_id
设置为
"*"
,表示接受任何网络 ID,通常在开发环境中使用。 为了更严谨的配置,应该指定对应的network_id。
以下是
truffle-config.js
文件的代码示例,展示了如何配置连接到 Ganache 的 development 网络,并指定 Solidity 编译器的版本:
module.exports = {
networks: {
development: {
host: "127.0.0.1", // Localhost (default: none)
port: 7545, // Standard Ethereum port (default: none)
network_id: "*", // Any network (default: none)
gas: 6721975, // Gas limit used for deployments
gasPrice: 20000000000 // Gas price used for deployments
},
},
compilers: {
solc: {
version: "0.8.0", // Fetch exact version from solc-bin (default: truffle's version)
settings: { // See the solidity docs for advice about optimization and gas consumption.
optimizer: {
enabled: false,
runs: 200
},
}
}
},
};
部署智能合约
在您的项目根目录下,使用 Truffle 框架执行以下命令,将智能合约部署到本地 Ganache 区块链网络:
truffle migrate
这条命令会读取并执行
migrations/
目录下的部署脚本。这些脚本通常包含将您的
SimpleStorage
智能合约部署到 Ganache 区块链所需的步骤。
truffle migrate
命令会自动处理合约的编译(如果尚未编译)和部署过程。
执行命令后,控制台将输出详细的部署信息。 这些信息包括:
-
合约地址:
这是
SimpleStorage
合约在 Ganache 区块链上部署后的唯一地址。 您需要记录这个地址,以便与合约进行交互。 - 交易哈希: 每次部署操作都会产生一个唯一的交易哈希。 这个哈希值可以用来在区块链浏览器中追踪部署交易。
- Gas 消耗: 部署合约所需的 Gas 数量。 Gas 是以太坊虚拟机(EVM)执行操作的燃料。 Gas 消耗量是评估合约部署成本的重要指标。
- 区块哈希: 包含此部署交易的区块的哈希值。
- 合约名称及文件路径: 显示被成功部署的合约名称及其在项目中的位置。
请仔细阅读控制台输出,确保部署过程成功完成。 任何错误信息都应仔细排查,以确保合约已正确部署。
编写测试用例
为了确保智能合约的功能符合预期,我们需要编写详尽的测试用例。在项目根目录下的
test/
目录下创建一个名为
SimpleStorage.test.js
的文件。该文件将包含使用 JavaScript 编写的测试代码,利用 Truffle 提供的测试框架与合约进行交互。
将以下代码添加到
SimpleStorage.test.js
文件中:
javascript const SimpleStorage = artifacts.require("SimpleStorage");
contract("SimpleStorage", (accounts) => { it("should set and get the stored data correctly", async () => { const simpleStorage = await SimpleStorage.deployed();
await simpleStorage.set(100, { from: accounts[0] });
const storedData = await simpleStorage.get();
assert.equal(storedData.toNumber(), 100, "The stored data should be 100");
}); });
这个测试用例采用了 Mocha 作为测试运行器,并使用 Chai 断言库来验证
SimpleStorage
合约的
set
和
get
函数的行为。它模拟了与合约的交互,并检查返回的结果是否与预期一致。通过运行这些测试,可以确保合约在各种情况下都能正常工作。
-
contract("SimpleStorage", (accounts) => { ... });
: 定义了一个测试套件(test suite),它将所有与SimpleStorage
合约相关的测试用例组织在一起。accounts
是一个数组,由 Ganache 或其他以太坊测试网络提供,包含了预先配置好的、可用于测试的以太坊账户地址。这些账户拥有测试用的以太币,可以用来支付交易费用。 -
it("should set and get the stored data correctly", async () => { ... });
: 定义了一个单独的测试用例(test case),专门用于测试set
和get
函数的组合行为。async
关键字表明这是一个异步函数,允许在测试中执行异步操作,如与区块链的交互。 -
const simpleStorage = await SimpleStorage.deployed();
: 获取已经部署到测试网络上的SimpleStorage
合约的实例。SimpleStorage.deployed()
函数返回一个 Promise,它会在合约成功部署后解析为一个合约对象。这个合约对象允许我们调用合约的函数。 -
await simpleStorage.set(100, { from: accounts[0] });
: 调用SimpleStorage
合约的set
函数,并将storedData
的值设置为 100。{ from: accounts[0] }
选项指定了交易的发送者地址为accounts
数组中的第一个账户。这模拟了由特定用户调用合约函数的过程,并允许合约记录调用者的身份(如果需要的话)。await
关键字确保交易被成功挖掘后再继续执行后续代码。 -
const storedData = await simpleStorage.get();
: 调用get
函数,从合约中检索storedData
的值。该函数也会返回一个 Promise,await
关键字用于等待 Promise 解析,并将返回的值赋给storedData
变量。 -
assert.equal(storedData.toNumber(), 100, "The stored data should be 100");
: 使用 Chai 断言库的assert.equal
函数来验证storedData
的值是否等于 100。由于从智能合约读取的数据通常以BigNumber
对象的形式返回,因此需要使用toNumber()
方法将其转换为 JavaScript 数字,以便进行比较。如果断言失败(即storedData
的值不等于 100),则测试将失败,并显示错误消息 "The stored data should be 100",帮助开发者快速定位问题。
运行测试
在项目根目录下,打开你的终端或命令提示符,然后执行以下命令来启动智能合约的测试流程:
bash
truffle test
上述命令会指示 Truffle 框架执行项目
test/
目录下的所有测试文件。Truffle 会编译你的合约,部署到测试网络(通常是 Ganache),然后运行你编写的测试脚本来验证合约的功能是否符合预期。
测试脚本通常使用 JavaScript 编写,并利用 Web3.js 或其他库与部署的智能合约进行交互。每个测试用例都会调用合约的函数,并使用断言来检查返回结果是否与预期值匹配。如果所有断言都通过,则该测试用例被认为是成功的。
如果所有测试都通过,你将在终端中看到绿色的 "✓" 符号,并且会显示每个测试用例的名称和执行时间。如果任何测试失败,你将看到红色的 "✗" 符号,以及失败原因的详细信息,例如断言错误或合约执行异常。通过查看这些错误信息,你可以快速定位并修复合约中的 bug。
为了确保你的智能合约的质量,编写充分且全面的测试至关重要。良好的测试覆盖率可以帮助你尽早发现潜在的问题,并提高合约的安全性和可靠性。
与合约交互
与部署到区块链上的智能合约交互是区块链应用开发的关键步骤。开发者可以通过多种方式与合约互动,其中两种常见且强大的工具是 Truffle Console 和 MetaMask。
Truffle Console: Truffle Console 提供了一个交互式的 JavaScript 环境,允许开发者直接与部署在本地或测试网络上的合约进行交互。它会自动加载合约的 ABI (Application Binary Interface) 和部署地址,简化了合约方法的调用过程。开发者可以使用 JavaScript 语法调用合约函数,发送交易,并查看返回结果。Truffle Console 是进行快速测试、调试和探索合约功能的理想选择。它还支持合约事件的监听,方便开发者实时监控合约的状态变化。
MetaMask: MetaMask 是一款流行的浏览器扩展钱包,它允许用户安全地管理其以太坊账户,并与去中心化应用程序 (DApps) 进行交互。对于已经部署到公共测试网络或主网的合约,MetaMask 提供了一个用户友好的界面,允许用户授权交易,并与合约进行交互。用户可以通过 MetaMask 连接到 DApp,调用合约函数,并使用他们的以太坊账户支付 Gas 费用。MetaMask 的主要优势在于其易用性和安全性,它使得普通用户也能轻松地参与到区块链生态系统中。通过 MetaMask,用户可以执行诸如发送代币、参与去中心化交易所、以及与各种其他类型的智能合约交互等操作。
选择使用 Truffle Console 还是 MetaMask 取决于具体的使用场景。对于开发和测试阶段,Truffle Console 通常更方便。而对于面向用户的 DApp,MetaMask 是一个更合适的选择。在实际开发中,开发者可能会同时使用这两种工具,以满足不同的需求。
使用 Truffle Console:
通过命令行启动 Truffle Console,它提供了一个交互式的 JavaScript 环境,连接到你的以太坊开发网络。这允许你直接与已部署的智能合约进行交互和测试。
bash truffle console
在 Truffle Console 中,你可以轻松获取已部署的合约实例,并调用其函数。以下是一个示例,展示了如何与一个名为 `SimpleStorage` 的合约交互,该合约包含 `get` 和 `set` 方法。
javascript SimpleStorage.deployed().then(instance => { simpleStorage = instance; // 获取合约实例并赋值给 simpleStorage 变量 return simpleStorage.get(); // 调用 get 函数,获取当前存储的值 }).then(value => { console.log(value.toNumber()); // 将返回的值转换为数字并打印到控制台 return simpleStorage.set(200, {from: web3.eth.accounts[0]}); // 调用 set 函数,将值设置为 200。`from` 指定交易的发送者 }).then(() => { return simpleStorage.get(); // 再次调用 get 函数,验证值是否已更新 }).then(value => { console.log(value.toNumber()); // 将更新后的值转换为数字并打印到控制台 });
上述代码片段使用了 Promises,这是 JavaScript 中处理异步操作的一种方式。`SimpleStorage.deployed()` 返回一个 Promise,该 Promise 在合约部署后会被解析为合约实例。`.then()` 方法允许你链式调用合约函数,并在每个步骤完成后执行相应的操作。
重要的是要注意 `from` 选项,它指定了用于发送交易的以太坊账户。在这个例子中,我们使用了 `web3.eth.accounts[0]`,它通常是 Ganache 或其他开发网络中第一个可用的账户。在实际应用中,你需要确保有足够的 ETH 余额来支付 gas 费用,否则交易可能会失败。
Truffle Console 还允许你访问 `web3` 对象,这是一个以太坊 JavaScript API,允许你与以太坊区块链进行交互。你可以使用 `web3` 对象来查询账户余额、发送交易、监听事件等等。
使用 MetaMask 与 Ganache 进行交互:
- 连接 MetaMask 至 Ganache 网络: 打开 MetaMask 扩展程序,在网络选择器中选择“自定义 RPC”。 填入 Ganache 提供的网络参数,包括网络名称(例如 "Ganache Network")、RPC URL(通常为 "http://127.0.0.1:7545" 或 "http://localhost:7545")和链 ID(通常为 1337)。 点击“保存”以添加并切换到 Ganache 网络。 确保MetaMask已连接到本地Ganache区块链。
- 导入 Ganache 账户至 MetaMask: 在 Ganache 界面中,找到默认生成的账户列表。 每个账户都有一个私钥。 将每个账户的私钥复制到剪贴板。 在 MetaMask 中,点击账户图标,选择“导入账户”。 将私钥粘贴到输入框中,然后点击“导入”。 对所有希望使用的 Ganache 账户重复此过程。成功导入后,账户余额将反映 Ganache 中分配的测试以太币。
-
获取 SimpleStorage 合约的 ABI 和地址:
SimpleStorage 合约部署后,编译工具(如 Truffle 或 Hardhat)会生成合约的应用二进制接口(ABI)和部署地址。 ABI 是一个 JSON 格式的文件,描述了合约的所有函数及其参数和返回类型。 地址是合约在区块链上的唯一标识符。 找到编译输出目录中的 ABI 文件(通常命名为
SimpleStorage.
或类似名称),并记录合约的部署地址。 部署地址通常在部署脚本的输出中可以找到。 - 在 MetaMask 中添加 SimpleStorage 合约: 打开 MetaMask 扩展程序,选择要使用的账户。 点击“添加合约”按钮。 选择“导入合约”选项。 将 SimpleStorage 合约的地址粘贴到“合约地址”字段中。 将完整的 ABI 内容(JSON 格式)粘贴到“合约 ABI”字段中。 点击“添加”按钮。 MetaMask 将解析 ABI 并显示合约的所有可调用函数。
-
使用 MetaMask 界面调用合约的
set
和get
函数: 在 MetaMask 中添加 SimpleStorage 合约后,你可以在合约界面中找到set
和get
函数。 要调用set
函数,在相应的输入框中输入要存储的新值,然后点击“写入”按钮。 MetaMask 将弹出一个交易确认窗口,显示交易详情,包括 gas 费用。 点击“确认”以提交交易。 要调用get
函数,直接点击“读取”按钮。 MetaMask 将发送一个只读调用,并在界面上显示合约中存储的当前值。 你可以通过检查Ganache的交易记录来确认set
函数的调用是否成功以及get
函数是否返回了正确的值。