Truffle 是针对基于以太坊的 Solidity 智能合约开发的一套开发框架,本身基于 Javascript。Truffle 对客户端做了深度集成,开发、测试、部署一行命令都可以搞定,不用再记那么多环境地址,繁重的配置更改,及记住诸多的命令。它提供了一套类似 maven 或 gradle 这样的项目构建机制,能自动生成相关目录,默认是基于 Web 的。当前这个打包机制是自定义的,比较简陋,不与当前流行打包方案兼容,但官方称会在以后弃用,要与主流方式兼容,不过现在它也支持自定义打包流程;
Truffle 还提供了合约抽象接口,可以直接通过 var meta = MetaCoin.deployed()
拿到合约对象后,在 Javascript 中直接操作对应的合约函数,原理是使用了基于 web3.js 封装的 Ether Pudding 工具包,简化了开发流程;
提供了控制台,在使用框架构建后可以直接在命令行调用输出结果,可极大方便开发调试;提供了文件变化监控,合约及配置变化后自动发布与部署,不用每次修改后都重走整个流程;
注意
注意:本教程正确性只针对教程中的指定开发环境,Truffle 及其依赖相关环境版本更新太快,不同的环境可能导致不同的错误或者结果,请自行踩坑!
搭建开发环境
本教程以在 MAC 上创建一个 ERC-20 标准合约为例,系统环境 macOS High Sierra 10.13.4.
安装node
本教程当前node版本:v9.4.0,具体安装过程略
安装npm
本教程当前npm版本:5.8.0,具体安装过程略
安装truffle
npm install -g truffle
本教程当前truffle版本信息:
Truffle v4.1.5 (core: 4.1.5)
Solidity v0.4.21 (solc-js)
版本不同,使用命令或者流程就可能不同,请自行踩坑!
官方文档:http://truffleframework.com/docs/
安装testrpc
npm install -g ethereumjs-testrpc
版本信息:EthereumJS TestRPC v6.0.3 (ganache-core: 2.0.2)
构建Truffle工程
创建工程
- 创建工程目录
dsa:contract-test anyhong$ mkdir erc20token-tutorial
dsa:contract-test anyhong$ cd erc20token-tutorial/
- 使用Truffle Box模板初始化工程
dsa:erc20token-tutorial anyhong$ truffle unbox tutorialtoken
Downloading...
Unpacking...
Setting up...
Unbox successful. Sweet!
Commands:
Compile: truffle compile
Migrate: truffle migrate
Test contracts: truffle test
Run dev server: npm run dev
这里使用 tutorialtoken 这个模板来初始化项目,更多配置模板:http://truffleframework.com/boxes/,注意可能有的模板在最新版本 Truffle 里运行异常,具体的自行踩坑!
- 创建完成后可以看到文件目录结构如下
dsa:erc20token-tutorial anyhong$ tree -L 1
.
├── box-img-lg.png
├── box-img-sm.png
├── bs-config.json
├── build
├── contracts // 合约文件目录
├── migrations // 部署配置文件
├── node_modules // npm组件
├── package-lock.json
├── package.json
├── src // web文件目录
├── test // 测试用例文件
└── truffle.js
contracts
:合约文件文件夹
migrations
:这个文件夹放合约迁移配置文件
src
:web文件夹,可以改写里面的文件,添加自己的测试代码等等
test
:测试用例文件夹,可以编写js或者Solidity测试用例
编写智能合约
基础合约
由于开发的是一个 ERC-20 标准的合约,所以构建一些可复用模板:
/// SafeMath.sol
/// 基础库
pragma solidity ^0.4.18;
/**
* @title SafeMath
* @dev Math operations with safety checks that throw on error
*/
library SafeMath {
/**
* @dev Multiplies two numbers, throws on overflow.
*/
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(c / a == b);
return c;
}
/**
* @dev Integer division of two numbers, truncating the quotient.
*/
function div(uint256 a, uint256 b) internal pure returns (uint256) {
// assert(b > 0); // Solidity automatically throws when dividing by 0
uint256 c = a / b;
// assert(a == b * c + a % b); // There is no case in which this doesn't hold
return c;
}
/**
* @dev Subtracts two numbers, throws on overflow (i.e. if subtrahend is greater than minuend).
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
assert(b <= a);
return a - b;
}
/**
* @dev Adds two numbers, throws on overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
/// ERC20Basic.sol
/// ERC20 token 基础库
pragma solidity ^0.4.18;
/**
* @title ERC20Basic
* @dev Simpler version of ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/179
*/
contract ERC20Basic {
function totalSupply() public view returns (uint256);
function balanceOf(address who) public view returns (uint256);
function transfer(address to, uint256 value) public returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
}
/// ERC20.sol
/// ERC20 token 基础库
pragma solidity ^0.4.18;
import "./ERC20Basic.sol";
/**
* @title ERC20 interface
* @dev see https://github.com/ethereum/EIPs/issues/20
*/
contract ERC20 is ERC20Basic {
function allowance(address owner, address spender) public view returns (uint256);
function transferFrom(address from, address to, uint256 value) public returns (bool);
function approve(address spender, uint256 value) public returns (bool);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
/// BasicToken.sol
/// ERC20 token 基础库
pragma solidity ^0.4.18;
import "./ERC20Basic.sol";
import "./SafeMath.sol";
/**
* @title Basic token
* @dev Basic version of StandardToken, with no allowances.
*/
contract BasicToken is ERC20Basic {
using SafeMath for uint256;
mapping(address => uint256) balances;
uint256 totalSupply_;
/**
* @dev total number of tokens in existence
*/
function totalSupply() public view returns (uint256) {
return totalSupply_;
}
/**
* @dev transfer token for a specified address
* @param _to The address to transfer to.
* @param _value The amount to be transferred.
*/
function transfer(address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[msg.sender]);
// SafeMath.sub will throw if there is not enough balance.
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
Transfer(msg.sender, _to, _value);
return true;
}
/**
* @dev Gets the balance of the specified address.
* @param _owner The address to query the the balance of.
* @return An uint256 representing the amount owned by the passed address.
*/
function balanceOf(address _owner) public view returns (uint256 balance) {
return balances[_owner];
}
}
/// StandardToken.sol
/// ERC20 token 基础库
pragma solidity ^0.4.18;
import "./BasicToken.sol";
import "./ERC20.sol";
/**
* @title Standard ERC20 token
*
* @dev Implementation of the basic standard token.
* @dev https://github.com/ethereum/EIPs/issues/20
* @dev Based on code by FirstBlood: https://github.com/Firstbloodio/token/blob/master/smart_contract/FirstBloodToken.sol
*/
contract StandardToken is ERC20, BasicToken {
mapping (address => mapping (address => uint256)) internal allowed;
/**
* @dev Transfer tokens from one address to another
* @param _from address The address which you want to send tokens from
* @param _to address The address which you want to transfer to
* @param _value uint256 the amount of tokens to be transferred
*/
function transferFrom(address _from, address _to, uint256 _value) public returns (bool) {
require(_to != address(0));
require(_value <= balances[_from]);
require(_value <= allowed[_from][msg.sender]);
balances[_from] = balances[_from].sub(_value);
balances[_to] = balances[_to].add(_value);
allowed[_from][msg.sender] = allowed[_from][msg.sender].sub(_value);
Transfer(_from, _to, _value);
return true;
}
/**
* @dev Approve the passed address to spend the specified amount of tokens on behalf of msg.sender.
*
* Beware that changing an allowance with this method brings the risk that someone may use both the old
* and the new allowance by unfortunate transaction ordering. One possible solution to mitigate this
* race condition is to first reduce the spender's allowance to 0 and set the desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
* @param _spender The address which will spend the funds.
* @param _value The amount of tokens to be spent.
*/
function approve(address _spender, uint256 _value) public returns (bool) {
allowed[msg.sender][_spender] = _value;
Approval(msg.sender, _spender, _value);
return true;
}
/**
* @dev Function to check the amount of tokens that an owner allowed to a spender.
* @param _owner address The address which owns the funds.
* @param _spender address The address which will spend the funds.
* @return A uint256 specifying the amount of tokens still available for the spender.
*/
function allowance(address _owner, address _spender) public view returns (uint256) {
return allowed[_owner][_spender];
}
/**
* @dev Increase the amount of tokens that an owner allowed to a spender.
*
* approve should be called when allowed[_spender] == 0. To increment
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _addedValue The amount of tokens to increase the allowance by.
*/
function increaseApproval(address _spender, uint _addedValue) public returns (bool) {
allowed[msg.sender][_spender] = allowed[msg.sender][_spender].add(_addedValue);
Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
/**
* @dev Decrease the amount of tokens that an owner allowed to a spender.
*
* approve should be called when allowed[_spender] == 0. To decrement
* allowed value is better to use this function to avoid 2 calls (and wait until
* the first transaction is mined)
* From MonolithDAO Token.sol
* @param _spender The address which will spend the funds.
* @param _subtractedValue The amount of tokens to decrease the allowance by.
*/
function decreaseApproval(address _spender, uint _subtractedValue) public returns (bool) {
uint oldValue = allowed[msg.sender][_spender];
if (_subtractedValue > oldValue) {
allowed[msg.sender][_spender] = 0;
} else {
allowed[msg.sender][_spender] = oldValue.sub(_subtractedValue);
}
Approval(msg.sender, _spender, allowed[msg.sender][_spender]);
return true;
}
}
/// TutorialToken.sol
/// ERC20 token
pragma solidity ^0.4.17;
import './StandardToken.sol';
contract TutorialToken is StandardToken {
string public name = 'Tutorial Test Token';
string public symbol = 'TTT';
uint8 public decimals = 2;
uint public INITIAL_SUPPLY = 100000;
function TutorialToken() public {
totalSupply_ = INITIAL_SUPPLY;
balances[msg.sender] = INITIAL_SUPPLY;
}
}
编译部署
- 进入开发开模式
启动后,默认开启RPC端口 http://127.0.0.1:9545/
, 默认提供10个账户:
dsa:erc20token-tutorial anyhong$ truffle develop
Truffle Develop started at http://127.0.0.1:9545/
Accounts:
(0) 0x627306090abab3a6e1400e9345bc60c78a8bef57
(1) 0xf17f52151ebef6c7334fad080c5704d77216b732
(2) 0xc5fdf4076b8f3a5357c5e395ab970b5b54098fef
(3) 0x821aea9a577a9b44299b9c15c88cf3087f3b5544
(4) 0x0d1d4e623d10f9fba5db95830f7d3839406c6af2
(5) 0x2932b7a2355d6fecc4b5c0b6bd44cc31df247a2e
(6) 0x2191ef87e392377ec08e7c08eb105ef5448eced5
(7) 0x0f4f2ac550a1b4e2280d04c21cea7ebd822934b5
(8) 0x6330a553fc93768f612722bb8c2ec78ac90b3bbc
(9) 0x5aeda56215b167893e80b4fe645ba6d5bab767de
Private Keys:
(0) c87509a1c067bbde78beb793e6fa76530b6382a4c0241e5e4a9ec0a0f44dc0d3
(1) ae6ae8e5ccbfb04590405997ee2d52d2b330726137b875053c36d94e974d162f
(2) 0dbbe8e4ae425a6d2687f1a7e3ba17bc98c673636790f1b8ad91193c05875ef1
(3) c88b703fb08cbea894b6aeff5a544fb92e78a18e19814cd85da83b71f772aa6c
(4) 388c684f0ba1ef5017716adb5d21a053ea8e90277d0868337519f97bede61418
(5) 659cbb0e2411a44db63778987b1e22153c086a95eb6b18bdf89de078917abc63
(6) 82d052c865f5763aad42add438569276c00d3d88a2d062d36b2bae914d58b8c8
(7) aa3680d5d48a8283413f7a108367c7299ca73f553735860a87b08f39395618b7
(8) 0f62d96d6675f32685bbdb8ac13cda7c23436f63efbb9d07700d8669ff12b7c4
(9) 8d5366123cb560bb606379f90a0bfd4769eecc0557f1b362dcae9012b548b1e5
Mnemonic: candy maple cake sugar pudding cream honey rich smooth crumble sweet treat
⚠️ Important ⚠️ : This mnemonic was created for you by Truffle. It is not secure.
Ensure you do not use it on production blockchains, or else you risk losing funds.
- 编译合约
编译过程中可能会有部分警告,暂时忽略。
truffle(develop)> compile
Compiling ./contracts/BasicToken.sol...
Compiling ./contracts/ERC20.sol...
Compiling ./contracts/ERC20Basic.sol...
Compiling ./contracts/Migrations.sol...
Compiling ./contracts/SafeMath.sol...
Compiling ./contracts/StandardToken.sol...
Compilation warnings encountered:
...(警告省略)
Writing artifacts to ./build/contracts
- 配置迁移部署文件
进入项目根目录 erc20token-tutoriall
,找到合约迁移配置目录 migrations
,发现下面已经存在 1_initial_migration.js
文件,再创建一个 2_deploy_contracts.js
, 加上代码如下:
var TutorialToken = artifacts.require("TutorialToken");
module.exports = function(deployer) {
deployer.deploy(TutorialToken);
};
- 迁移部署合约
truffle(develop)> migrate
Using network 'develop'.
Running migration: 1_initial_migration.js
Replacing Migrations...
... 0x16d5cea87a2b3f70f05a318dca116c03bae8121387b709302d072dc0ec1278d0
Migrations: 0x2a504b5e7ec284aca5b6f49716611237239f0b97
Saving successful migration to network...
... 0xad16ddf12b1be2f43938221b5fd7dfa05dae1c6da09776f16f25b2e4a7f4cedc
Saving artifacts...
Running migration: 2_deploy_contracts.js
Deploying TutorialToken...
... 0x183e81e752df912b30a5f52e0b86e3396f87dec37e468b9053867b3e52b9a36b
TutorialToken: 0x2eca6fcfef74e2c8d03fbaf0ff6712314c9bd58b
Saving successful migration to network...
... 0x05d263b8571d786abcca55bf0248d2b0185697ae076b19bf3403015a7ada9f6b
Saving artifacts...
测试用例
测试用例可以用 js 或者 solidity 写,放在在 test
目录下。
- js测试用例
/// TestErc20token.js
var TutorialToken = artifacts.require("./TutorialToken.sol");
contract('TutorialToken', function(accounts) {
it("should put 100000 token in the first account", function() {
return TutorialToken.deployed().then(function(instance) {
return instance.balanceOf.call(accounts[0]);
}).then(function(balance) {
assert.equal(balance.valueOf(), 100000, "100000 wasn't in the first account");
});
});
});
- solidity测试用例
/// TestErc20token.sol
pragma solidity ^0.4.17;
import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/TutorialToken.sol";
contract TestErc20token {
function testInitialBalanceUsingDeployedContract() public {
TutorialToken token = TutorialToken(DeployedAddresses.TutorialToken());
uint expected = 100000;
Assert.equal(token.balanceOf(tx.origin), expected, "Owner should have 100000 token initially");
}
}
在 develop 模式下输入 test 命令开始运行测试用例:
truffle(develop)> test
Using network 'develop'.
TestErc20token
✓ testInitialBalanceUsingDeployedContract (66ms)
Contract: TutorialToken
✓ should put 100000 token in the first account
2 passing (1s)
如上表示测试用例通过。
浏览器运行DApp
- 推荐使用 Google Chrome,并安装 MetaMask
详细步骤略,具体安装过程参考 Installing and configuring MetaMask。
MetaMask能快速账户切换账户,查看交易信息,方便调试,
注意:添加 Custom RPC 的地址为:http://127.0.0.1:9545/
要与前面的 RPC 地址对应起来。
- 运行 liteserver development server
dsa:erc20token-tutorial anyhong$ npm run dev
> tutorialtoken@1.0.0 dev /Users/anyhong/Desktop/区块链/以太坊/合约/contract-test/erc20token-tutorial
> lite-server
** browser-sync config **
{ injectChanges: false,
files: [ './**/*.{html,htm,css,js}' ],
watchOptions: { ignored: 'node_modules' },
server:
{ baseDir: [ './src', './build/contracts' ],
middleware: [ [Function], [Function] ] } }
[Browsersync] Access URLs:
---------------------------------------
Local: http://localhost:3000
External: http://192.168.232.46:3000
---------------------------------------
UI: http://localhost:3001
UI External: http://192.168.232.46:3001
---------------------------------------
[Browsersync] Serving files from: ./src
[Browsersync] Serving files from: ./build/contracts
[Browsersync] Watching files...
18.04.13 14:18:29 200 GET /index.html
18.04.13 14:18:30 200 GET /css/bootstrap.min.css
18.04.13 14:18:30 200 GET /js/bootstrap.min.js
18.04.13 14:18:30 200 GET /js/app.js
18.04.13 14:18:30 200 GET /js/web3.min.js
18.04.13 14:18:30 200 GET /js/truffle-contract.js
18.04.13 14:18:30 200 GET /TutorialToken.json
http://localhost:3000
打开 web 调试窗口.
参考文章
Truffle官方文档
一起学习以太坊
区块链truffle 4.1.5 开发入门
Truffle部署、编译、测试智能合约的完整实践操作
Truffle boxes
Etherum pet shop
Building robust smart contracts with openzeppelin