Truffle tutorial

Anyhong 2017年06月16日 3,455次浏览

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