1. 介绍
Truffle套件是一个开发以太坊 (Ethereum) 区块链DApp (分布式应用程序) 的开发环境,是开发DApp的一站式解决方案,功能包括: 编译合约、部署合约、开发DApp前端、测试DApp等。
1.1 三个模块
Truffle套件主要有3个模块:
- Truffle:以太坊 (Ethereum) 区块链DApp开发环境。
- Ganache:Ganache可以创建本地区块链网络,用于测试智合约,你可以在本地区块链网络上部署合约、开发应用程序、运行测试和执行其他任务,不需要付任何费用。
- Drizzle:前端库的集合,使编写Dapp用户界面更容易。
1.2 Truffle Ethereum 特性
- 内置智能合约编译、链接、部署和二进制管理功能。
- 用于快速开发的自动化合约测试功能。
- 脚本化、可扩展的合约部署和迁移框架。
- 强大的网络管理功能,可以部署到任意数量的公共和私有网络。
- 使用EthPM和NPM进行包管理,使用ERC190标准。
- 交互式控制台工具可以直接与合约通信。
- 可配置的构建管道,支持紧密集成。
- 强大的外部脚本运行器功能,支持在Truffle环境中执行外部脚本。
2. 安装
2.1 安装Node 和 NPM
2.2 安装Truffle
安装:
$ npm install -g truffle
查看版本:
$ truffle version
3. 创建项目
可以使用 truffle init 命令可以创建原始的项目模板,但是对于初学者,使用 Truffle Box 更简单,Truffle Box中包含了很多示例应用程序和项目模板。我们将使用 MetaCoin box ,它将创建一个通证,可以在账户之间传输。
github上下载truffle各模板地址:https://github.com/truffle-box
我们下载的是matacoin-box:https://github.com/truffle-box/metacoin-box
打开文件,如下所示:
合约编译后生成的构建默认地址是 build/contracts
但可以改。在 truffle-config.js 中 通过 contracts_build_directory: ‘’ 修改位置。
创建项目可以直接用指令:truffle unbox
4. 编译合约
要编译Truffle项目,切换到项目根目录,然后在终端中执行以下命令:
truffle compile
在第一次执行时,将编译contracts文件夹下的所有合约。在随后的执行中,Truffle将只编译更改过的合约。如果你想全部编译,可以使用 --all 选项执行上面的命令。
4.1 编译构件
编译的构件将放在项目根目录下的build/contract/目录中(默认,可更换), 如果此目录不存在,则创建该目录。这些构件是Truffle内部工作不可或缺的,它们在应用程序的成功部署中扮演着重要的角色。你不应该编辑这些文件,在编译和部署合约时,它们将被覆盖。
4.2 依赖项
使用Solidity语言的 import 命令声明合约依赖项
1、通过文件名导入依赖项
import "./AnotherContract.sol";
这将导入另一个源文件 AnotherContract.sol 中的所有合约,导入文件的路径是基于当前源文件的相对路径。
2、从外部包导入合约
import "somepackage/SomeContract.sol";
这里,somepackage表示通过 EthPM 或 NPM 安装的包。SomeContract.sol表示该包提供的Solidity源文件。
注意,在搜索NPM安装的包之前,Truffle将首先搜索EthPM安装的包,因此在命名冲突的罕见情况下,将使用EthPM安装的包。
5. 部署 (迁移) 合约
迁移脚本是JavaScript文件,用于将合约部署到Ethereum网络。
migrations文件夹下存放所有的迁移脚本。
若运行迁移,执行以下命令(默认是development,也就是本地ganache网络):
$ truffle migrate
若要指定特定网络:
truffle migrate --network goerli
简单地说,迁移脚本就是一组托管的部署脚本。
在 Truffle 中,migrate 命令用于部署 Solidity 智能合约到区块链网络中。当你多次运行 migrate 命令时,Truffle 会跟踪每个智能合约的部署,并尝试在之前的部署基础上进行更新。如果你的合约代码已经改变了,这可能会导致一些问题。
使用 --reset 标志可以解决这个问题。这个标志告诉 Truffle 忽略之前的部署记录,并重新部署所有合约。这将确保你的合约代码和部署状态是最新的,并可以避免由于部署记录问题而导致的错误。
truffle migrate --reset --network goerli
5.1 迁移脚本文件
const ConvertLib = artifacts.require("ConvertLib"); // 通过artifacts.require方法告诉truffle,希望与哪些合约进行交互
const MetaCoin = artifacts.require("MetaCoin"); // 此方法与Node的require类似,它返回一个合约抽象,后续代码可以使用该抽象。方法参数是合约名称
module.exports = function (deployer,network,account) { // 所有迁移脚本都必须通过module.exports导出一个函数,
// 该函数接受deployer对象作为其第一个参数。deployer对象是执行部署任务的主接口
if (network == 'goerli') {
console.log("--------------------我是goerli-------------------")
console.log("当前网络账户:" + account);
}
deployer.deploy(ConvertLib,{from: "0x8038F0BF1CE32A31325BC4e166fcaCCFB171d1d6"}); // 使用部署器deployer来部署合约
deployer.link(ConvertLib, MetaCoin);
deployer.deploy(MetaCoin);
};
直接通过命令运行即可部署两个合约,ConvertLib 和 MetaCoin。
对于上述代码:
5.2 artifacts.require()
告诉Truffle,希望与哪些合约进行交互。此方法与Node的require类似,它返回一个合约抽象,后续代码可以使用该抽象。 方法参数是合约名称,不要传递源文件的名称,因为一个源文件可以包含多个合约。
5.3 module.exports
所有迁移脚本都必须通过 module.exports 导出一个函数。
第一个参数 deployer ,deployer对象是执行部署任务的主接口。
第二个参数 network,用于获取当前的网络。可以在配置文件truffle-config中配置网络节点:比如我配置了私有链网络节点 (Ganache)。
在部署合约时,可以指定到该网络:
truffle migrate --network live
当不指定网络直接 truffle migrate 迁移时,会自动选取本地的网络,这里我在未指定时自动选取的就是我的 Ganache 私有链网络。
第三个参数 accounts,可获取到当前网络的所有账户
5.4 Deployer API
5.4.1 deployer.deploy(contract, args…, options)
迁移脚本中,使用部署器 (Deployer) 来部署合约,部署器(Deployer)将按正确的代码顺序执行
这个函数部署指定合约,可以传入合约构造函数的参数。
- contract 要部署的合约
- args… 合约构造函数参数
- options 部署选项
部署后将覆盖以前的合约地址(即 Contract.address 将等于新部署的地址)。
// 部署单个合约,不传入构造函数参数
deployer.deploy(A);
// 使用构造函数参数部署单个合约
deployer.deploy(A, arg1, arg2, ...);
// 如果已经部署了此合约,设置不要部署它
deployer.deploy(A, {overwrite: false});
// 设置部署的gas量上限,及“from”地址
deployer.deploy(A, {gas: 4612388, from: "0x...."});
可以设置 from 地址,当不设置 from 地址时,默认的是 网络中的第一个账户。
5.4.2 deployer.link(library, destinations)
这个函数将已部署的库链接到一个或多个合约。目标可以是单个合约,也可以是合约数组。如果目标中有合约不依赖于被链接的库,则该合约将被忽略。
// 部署合约A,然后将contract A链接到contract B,然后部署B。
deployer.deploy(A);
deployer.link(A, B);
deployer.deploy(B);
// 将A链接到多个合约
deployer.link(A, [B, C, D]);
5.4.3 deployer.then(function() {…})
deployer的同步机制。
var a, b;
deployer.then(function() {
// 创建a的新版本
return A.new();
}).then(function(instance) {
a = instance;
// 获取B的已部署实例
return B.deployed();
}).then(function(instance) {
b = instance;
// 通过B的setA()函数在B上设置A的address新实例。
return b.setA(a.address);
});
6. 与合约交互
自己编写原始请求,与以太坊网络中的智能合约进行交互,是一件相当繁琐的工作。
Truffle简化了这个工作,利用Truffle提供的功能,我们可以方便地与合约进行交互。
以太坊网络中,对于向网络写入数据,以及从网络读取数据的2种行为作了区分通常,写入数据称为交易,而读取数据称为调用。交易和调用的处理方式非常不同。
6.1 交易
交易会向网络写入数据,改变网络状态。
交易可以是简单地向一个账户发送以太币,也可以复杂到执行合约函数或向网络添加新合约。
交易有运行成本,需要消耗Gas,Gas需要用以太币支付。
交易处理需要消耗时间,不会马上返回。如果你通过交易执行合约的函数,该函数不会马上返回,因为交易不是立即处理的。通常,通过交易执行的函数不会有返回值,而是返回一个交易哈希。
总结交易的特点:
- 有执行成本,消耗Gas
- 会更改网络的状态
- 不立即处理
- 没有返回值 (返回交易哈希) 。
6.2 调用
与交易相比,调用是非常不同的,调用是只读的。
调用可用于在以太坊网络上执行代码,但不会更改网络中的数据。
调用可以免费执行。
当通过调用执行合约函数时,将立即收到返回值。
总结调用的特点:
- 免费,不消耗Gas
- 不改变网络的状态
- 立即处理
- 有返回值
是交易还是调用,只需看是否向网络写入数据。
6.3 合约抽象对象
合约抽象对象在Javascript中表示一个合约,可以使用合约抽象对象与以太坊网络中的合约进行交互。
truff-contract 包提供对合约抽象的支持。
前面提到过,获取合约抽象对象的方法之一是,使用 **artifacts.require()**函数获取。
接下来看看truffle提供的js对象 MetaCoin.sol
// SPDX-License-Identifier: MIT
// Tells the Solidity compiler to compile only from v0.8.13 to v0.9.0
pragma solidity ^0.8.13;
import "./ConvertLib.sol";
// This is just a simple example of a coin-like contract.
// It is not ERC20 compatible and cannot be expected to talk to other
// coin/token contracts.
contract MetaCoin {
mapping (address => uint) balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
constructor() {
balances[tx.origin] = 10000;
}
function sendCoin(address receiver, uint amount) public returns(bool sufficient) {
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[receiver] += amount;
emit Transfer(msg.sender, receiver, amount);
return true;
}
function getBalanceInEth(address addr) public view returns(uint){
return ConvertLib.convert(getBalance(addr),2);
}
function getBalance(address addr) public view returns(uint) {
return balances[addr];
}
}
通过 truffle console 指令打开 truffle 控制台。
可以指定网络 truffle console --network goerli ,不指定的话默认情况下是本地
在truffle控制台中,调用 deployed() 返回合约抽象对象并查看:
6.4 执行交易
使用合约抽象,可以方便地在以太坊网络上执行合约函数。
let accounts = await web3.eth.getAccounts() // 获取所有账户
instance.sendCoin(accounts[1], 10, {from: accounts[0]}) // 调用合约的sendCoin写函数,会在ganache中产生一个调用合约的交易
交易参数 – sendCoin函数没有第3个参数,我们传入了第三个参数 {from: accounts[0]},这个参数是一个特殊参数,称为交易参数,它总是可以作为最后一个参数传递给一个函数,用于设置交易的相关细节。
这里我们设置了交易来源地址,确保该交易来自accounts[0](其实不设置的话默认也是account[0] ),交易参数中,可以设置以下选项:
- from
- to
- gas
- gasPrice
- value
- data
- nonce
6.5 执行调用
执行 getBalance 函数,该函数从网络中读取数据,不做任何更改,它只返回指定地址的MetaCoin余额。
instance.getBalance(accounts[0])
7. Ganache连接MetaMask
MetaMask上添加网络
这样就行了。但默认不会展示 ganache 本身钱包的地址。需要手动导入。
还有一种方法是卸载 metamask 重新下。导入助记词时导入ganache 自身钱包的助记词,这样就会直接把 ganache 中的多个账户展示在 metamask。