第五课 以太坊开发框架Truffle从入门到实战

962 阅读5分钟

【本文目标】
通过本文的学习和时间,你将熟悉以太坊开发框架Truffle的配置和运行,并借助Truffle完成一个智能合约的部署。

【技术收获】 通过本文的学习,你将掌握以下内容: 1,了解TRUFFLE的功能 2,了解TRUFFLE的安装,配置和启动 3,借助TRUFFLE完成METACOIN一个智能合约的运行 4,Testrpc,Geth环境的使用

【实操课程列表】 第一课 如何在WINDOWS环境下搭建以太坊开发环境

第二课 如何实现以太坊最简智能合约“Hello World”的运行

第四课 以太坊开发框架Truffle从入门到实战

第六课 技术小白如何开发一个DAPP区块链应用(以宠物商店为例)

第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易

第八课 如何调试以太坊官网的智能合约众筹案例

【说明】未列出的课程为知识普及的非实操类课程,所有区块链文章参考“区块链入口”专栏。

1. TRUFFLE是什么?

Truffle是一个世界级的开发环境,测试框架,以太坊的资源管理通道,致力于让以太坊上的开发变得简单,Truffle有以下:

  • 内置的智能合约编译,链接,部署和二进制文件的管理。
  • 快速开发下的自动合约测试。
  • 脚本化的,可扩展的部署与发布框架。
  • 部署到不管多少的公网或私网的网络环境管理功能
  • 使用EthPM&NPM提供的包管理,使用ERC190标准。
  • 与合约直接通信的直接交互控制台(写完合约就可以命令行里验证了)。
  • 可配的构建流程,支持紧密集成。
  • 在Truffle环境里支持执行外部的脚本。 【说明】更多以太坊术语可参考此篇文章: www.jianshu.com/p/036661986…

1.1 TRUFFLE的安装

在Ubuntu命令上窗口输入以下命令,完成安装:

$ npm install -g truffle

如果安装成功,可输入truffle version名称,正常情况下会有版本显示:

truffle version

环境要求

NodeJS 5.0+ Windows,Linux(推荐Ubuntu),或Mac OS X

Truffle客户端

有许多的以太坊客户端可以选择。我们推荐在开发和部署时使用不同客户端。 适用开发的客户端

当开发基于Truffle的应用时,我们推荐使用EthereumJS TestRPC。它是一个完整的在内存中的区块链仅仅存在于你开发的设备上。它在执行交易时是实时返回,而不等待默认的出块时间,这样你可以快速验证你新写的代码,当出现错误时,也能即时反馈给你。它同时还是一个支持自动化测试的功能强大的客户端。Truffle充分利用它的特性,能将测试运行时间提速近90%。

适用正式发布的客户端

对此有许多官方和非官方的以太坊客户端可供选择。最好使用TestRPC客户端充分测试后,再使用这些客户端。这些是完整的客户端实现,包括挖矿,网络,块及交易的处理,Truffle可以在不需要额外配置的情况下发布到这些客户端。

当发布到私有网络中

私人网络中使用了相同的技术,但却有不同的配置。所以你可以将上面提及的客户端来运行一个私有的网络,部署到这样的网络也是使用同样的方式。 【说明】作者使用TestRPCGeth (go-ethereum)这2种客户端,他们的安装方式参考文章:www.jianshu.com/p/683ea7d62…

2. 下载TRUFFLE MetaCoin样例进行环境搭建实战

2.1 MetaCoin初始化

我们假设前面的安装和环境搭建已全部成功,此时应该可以直接使用命令truffle了,下面我们建立一个工作间truffle-workspace,然后在工作间执行:

mkdir MetaCoin
cd MetaCoin
truffle unbox metacoin

原来使用truffle init,但现在它存在于unbox。

执行截图如下:

下载样例

unbox

Truffle 的盒子Boxs装有很多非常实用的项目样板,可以让你忽略一些环境配置问题,从而可以集中与开发你自己的DApp的业务唯一性。除此之外,Truffle Boxes能够容纳其他有用的组件、Solidity合约或者库,前后端视图等等。所有这些都是一个完整的实例Dapp程序。都可以下载下来逐一研究,寻找适合自己公司目前业务模型的组件。

Truffle的官方Boxes地址

可以看到,现在官方盒子还不多,总共7个,有三个是关于react的,两个是truffle自己的项目,可以下载体验,剩下两个是我们比较关心的,一个是metacoin,非常好的入门示例,另一个是webpack,顾名思义,它是一套比起metacoin更加完整的模板的存在。既然我们是初学,下面我们就从metacoin入手学习。

1) tutorialtoken 1] This box has all you need to get started with our Open Zeppelin (TutorialToken) tutorial. 2] truffleframework.com/boxes/tutor…

2)PET-SHOP 1] This box has all you need to get started with our Pet Shop tutorial. 2] truffleframework.com/boxes/pet-s…

3) METACOIN truffle unbox metacoin

4) ENDLESS-NAMELESS-INC/CHESHIRE(加密猫) 1] An Ethereum testnet running the CryptoKitties smart contracts An HTTP server running a minimal implementation of the CryptoKitties web API: A simple Node.js framework for seeding the development environment with realistic data and bootstraping your dApp. 2] truffleframework.com/boxes/chesh…

2.2 目录结构及文件解读

进入metacoin目录,当前目录已经被初始化成一个新的空的以太坊工程,目录结构如下:

contracts * ConvertLib.sol * MetaCoin.sol * Migrations.sol * .placeholder migrations * 1_initial_migration.js * 2_deploy_contracts.js test * metacoin.js * TestMetacoin.sol * .placeholder

  • truffle-config.js
  • truffle.js

初始化文件解释1:Migrations.sol

pragma solidity ^0.4.2;

contract Migrations {
  address public owner;
  uint public last_completed_migration;

  modifier restricted() {
    if (msg.sender == owner) _;
  }

  function Migrations() public {
    owner = msg.sender;
  }

  function setCompleted(uint completed) public restricted {
    last_completed_migration = completed;
  }

  function upgrade(address new_address) public restricted {
    Migrations upgraded = Migrations(new_address);
    upgraded.setCompleted(last_completed_migration);
  }
}

上面我们学习了Solidity具体的类型语法,我们来分析一下这个文件:

  • 它定义了一个名字为“迁移”的合约
  • 有一个任意访问的全局变量,存储于storage的地址类型变量owner
  • 有一个可任意访问的全局变量,存储于storage的无符号整型类型的变量last_completed_migration
  • modifier下面细说,此处略过
  • msg.sender下面细说,此处略过
  • 构造函数,初始化将发送方赋值给owner保存
  • 一个setCompleted赋值方法,赋值给last_completed_migration,其中该方法被声明为restricted,下面细说,此处略过
  • upgrade方法,调用当前合约自己的方法,得到合约的实例upgraded,然后通过该是咧调用setCompleted赋值方法。
Solidity语法补充说明1:function modifier

modifier的使用方法,就看上面的Migrations合约的例子即可,它可以自动改变函数的行为,例如你可以给他预设一个条件,他会不断检查,一旦符合条件即可走预设分支。它可以影响当前合约以及派生合约。

pragma solidity ^0.4.11;

contract owned {
    function owned() public { owner = msg.sender; }
    address owner;
    // 这里仅定义了一个modifier但是没有使用,它将被子类使用,方法体在这里“_;”,这意味着如果owner调用了这个函数,函数会被执行,其他人调用会抛出一个异常。
    modifier onlyOwner {
        require(msg.sender == owner);
        _;
    }
}

// 通过is关键字来继承一个合约类,mortal是owned的子类,也叫派生类。
contract mortal is owned {
    // 当前合约派生了owned,此方法使用了父类的onlyOwner的modifier
    // public onlyOwner, 这种写法挺让人困惑,下面给出了我的思考,暂理解为派生类要使用基类的modifier。
    function close() public onlyOwner {
        selfdestruct(owner);
    }
}

contract priced {
    // Modifiers可以接收参数
    modifier costs(uint price) {
        // 这里modifier方法体是通过条件判断,是否满足,满足则执行“_;”分支。
        if (msg.value >= price) {
            _;
        }
    }
}

contract Register is priced, owned {
    mapping (address => bool) registeredAddresses;
    uint price;

    // 构造函数给全局变量price赋值。
    function Register(uint initialPrice) public { price = initialPrice; }

    // payable关键字重申,如果不声明的话,函数关于以太币交易的操作都会被拒回。
    function register() public payable costs(price) {
        registeredAddresses[msg.sender] = true;
    }

    // 此派生类也要使用基类的modifier。
    function changePrice(uint _price) public onlyOwner {
        price = _price;
    }
}

contract Mutex {
    bool locked;
    modifier noReentrancy() {
        require(!locked);
        locked = true;
        _;
        locked = false;
    }

    function f() public noReentrancy returns (uint) {
        require(msg.sender.call());
        return 7;
    }
}

又延伸出来一个盲点:require关键字,它是错误判断,提到assert就懂了,官方文档的解释为:

require(bool condition):
throws if the condition is not met - to be used for errors in inputs or external components.

总结一下modifier:

  • 声明modifier时,特殊符号“_;”的意思有点像TODO,是一个“占位符”,指出了你要写的具体方法体内容的位置。
  • function close() public onlyOwner,派生类某方法想“如虎添翼”加入基类的某个modifier功能,就可以这样写,这行的具体意思就是:close方法也必须是owner本人执行,否则报错!
Solidity语法补充说明2:Restricting Access

限制访问一种针对合约的常见模式。但其实你永远不可能限制得了任何人或电脑读取你的交易内容或者你的合同状态。你可以使用加密增大困难,但你的合约就是用来读取数据的,那么其他人也会看到。所以,其实上面的modifier onlyOwner是一个特别好的可读性极高的限制访问的手段。

那么restricted关键字如何使用呢?

好吧,我刚刚带着modifier的知识重新看了上面的Migrations合约的内容发现,restricted并不是关键字,而是modifier的方法名,在其下的想增加该modifier功能的函数中,都使用了public restricted的方式来声明。

说到这里,我又明白了为什么要使用public onlyOwner这种写法,因为public是函数可见性修饰符,onlyOwner是自定义的限制访问的modifier方法,他们都是关于函数使用限制方面的,所以会写在一起,可以假想一个括号将它俩括起来,他们占一个位置,就是原来属于public|private|internal|external的那个位置。

Solidity语法补充说明3:Special Variables and Functions

这一点很重要了,我们研究一下Solidity自身携带的特殊变量以及函数:

  1. block.blockhash(uint blockNumber) returns (bytes32): 返回参数区块编号的hash值。(范围仅限于最近256块,还不包含当然块)
  2. block.coinbase (address): 当前区块矿工地址
  3. block.difficulty (uint): 当前区块难度
  4. block.gaslimit (uint): 当前区块的gaslimit
  5. block.number (uint): 当前区块编号
  6. block.timestamp (uint): 当前区块的timestamp,使用UNIX时间秒
  7. msg.data (bytes): 完整的calldata
  8. msg.gas (uint): 剩余的gas
  9. msg.sender (address): 信息的发送方 (当前调用)
  10. msg.sig (bytes4): calldata的前四个字节 (i.e. 函数标识符)
  11. msg.value (uint): 消息发送的wei的数量
  12. now (uint): 当前区块的timestamp (block.timestamp别名)
  13. tx.gasprice (uint): 交易的gas单价
  14. tx.origin (address): 交易发送方地址(完全的链调用)

msg有两个属性,一个是msg.sender,另一个是msg.value,这两个值可以被任何external函数调用,包含库里面的函数。

注意谨慎使用block.timestamp, now and block.blockhash,因为他们都是有可能被篡改的。

初始化文件解释2:MetaCoin.sol

pragma solidity ^0.4.18;

import "./ConvertLib.sol";

// 这是一个简单的仿币合约的例子。它并不是标准的可兼容其他币或token的合约,
// 如果你想创建一个标准兼容的token,请转到 https://github.com/ConsenSys/Tokens(TODO:一会儿我们再过去转)

contract MetaCoin {
        mapping (address => uint) balances;// 定义了一个映射类型变量balances,key为address类型,值为无符整型,应该是用来存储每个账户的余额,可以存多个。

        event Transfer(address indexed _from, address indexed _to, uint256 _value);// Solidity语法event,TODO:见下方详解。

        function MetaCoin() public {// 构造函数,tx.origin查查上面,找到它会返回交易发送方的地址,也就是说合约实例创建时会默认为当前交易发送方的余额塞10000,单位应该是你的仿币。
                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;// 否则从发送方余额中减去发送值,注意Solidity也有 “-=”,“+=” 的运算符哦
                balances[receiver] += amount;// 然后在接收方的余额中加入发送值数量。
                Transfer(msg.sender, receiver, amount);// 使用以上event关键字声明的方法
                return true;
        }

        function getBalanceInEth(address addr) public view returns(uint){// 获取以太币余额
                return ConvertLib.convert(getBalance(addr),2);// 调用了其他合约的方法,TODO:稍后介绍ConvertLib合约时说明。
        }

        function getBalance(address addr) public view returns(uint) {// 获取当前账户的仿币余额
                return balances[addr];
        }
}

Solidity语法补充说明4:Events

Events allow the convenient usage of the EVM logging facilities, which in turn can be used to “call” JavaScript callbacks in the user interface of a dapp, which listen for these events. Events提供了日志支持,进而可用于在用户界面上“调用”dapp JavaScript回调,监听了这些事件。简单来说,我们的DApp是基于web服务器上的web3.js与EVM以太坊结点进行交互的,而智能合约是部署在EVM以太坊结点上的。举一个例子:

contract ExampleContract {
  // some state variables ...
  function foo(int256 _value) returns (int256) {
    // manipulate state ...
    return _value;
  }
}

合约ExampleContract有个方法foo被部署在EVM的一个结点上运行了,此时用户如果想在DApp上调用合约内部的这个foo方法,如何操作呢,有两种办法:

  1. var returnValue = exampleContract.foo.call(2);// 通过web3 的message的call来调用。
  2. 合约内部再声明一个event ReturnValue(address indexed _from, int256 _value);并在foo方法内使用该event用来返回方法执行结果。

第一种办法在方法本身比较耗时的情况下会阻塞,或者不会获取到准确的返回值。所以采用第二种办法:就是通过Solidity的关键字event。event在这里就是一个回调函数的概念,当函数运行结束以后(交易进块),会通过event返回给web3,也就是DApp用户界面相应的结果。这是以太坊一种客户端异步调用方法。关于这个回调,要在DApp使用web3时显示编写:

exampleEvent.watch(function(err, result) {
  if (err) {
    console.log(err)
    return;
  }
  console.log(result.args._value)
  // 检查合约方法是否反返回结果,若有则将结果显示在用户界面并且调用exampleEvent.stopWatching()方法停止异步回调监听。
})

写Solidity最大的不同在于,我们要随时计算好我们的gas消耗,方法的复杂度,变量类型的存储位置(memory,storage等等)都会决定gas的消耗量。

使用event可以获得比storage更便宜的gas消耗。

总结一下event,就是如果你的Dapp客户端web3.js想调用智能合约内部的函数,则使用event作为桥梁,它能方便执行异步调用同时又节约gas消耗。

初始化文件解释3:ConvertLib.sol
pragma solidity ^0.4.4;

library ConvertLib{
        function convert(uint amount,uint conversionRate) public pure returns (uint convertedAmount)
        {
                return amount * conversionRate;
        }
}

与MetaCoin智能合约不同的是,ConvertLib是由library声明的一个库,它只有一个方法,就是返回给定的两个无符整数值相乘的结果。返回到上面的MetaCoin中该库的使用位置去分析,即可知道,MetaCoin的仿币的价格是以太币的一倍,所以MetaCoin是以以太币为标杆,通过智能合约发布的一个token,仿币。

这似乎就可以很好地解决我在《以太坊RPC机制与API实例》文章中需要发布三倍以太币的token的需求了,而我们完全不必更改以太坊源码,但那篇文章通过这个需求的路线研究了以太坊的Go源码也算功不可没。

初始化文件解释4:1_initial_migration.js

var Migrations = artifacts.require("./Migrations.sol");

module.exports = function(deployer) {
  deployer.deploy(Migrations);
};

这个js文件是nodejs的写法,看上去它的作用就是部署了上面的Migrations智能合约文件。

初始化文件解释5:2_deploy_contracts.js

var ConvertLib = artifacts.require("./ConvertLib.sol");
var MetaCoin = artifacts.require("./MetaCoin.sol");

module.exports = function(deployer) {
  deployer.deploy(ConvertLib);
  deployer.link(ConvertLib, MetaCoin);
  deployer.deploy(MetaCoin);
};

这个文件是meatcoin智能合约的部署文件,里面约定了部署顺序,依赖关系。这里我们看到了MetaCoin智能合约是要依赖于库ConvertLib的,所以要先部署ConvertLib,然后link他们,再部署MetaCoin,这部分js的写法可以参照官方文档DEPLOYER API,主要就是介绍了一下deploy、link以及then三个方法的详细用法,不难这里不再赘述。

初始化文件解释6:truffle-config.js, truffle.js

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
};
module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
};

这两个文件也都是nodejs,他们都是配置文件,可能作用域不同,目前它俩是完全相同的(因为啥也没有)。我们去它推荐的网站看一看。给出了一个例子:

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*" // Match any network id
    }
  }
};

这个例子展示了该配置文件可以配置网络环境,暂先到这,以后遇上了针对该配置文件进行研究。

初始化文件解释7:.placeholder

This is a placeholder file to ensure the parent directory in the git repository. Feel free to remove.

翻译过来就是:placeholder文件是用来保证在git库中父级目录的,可以删除。

初始化文件解释8:metacoin.js

和下面的文件一样,他们的功能都是用来做单元测试的,truffle在编译期间会自动执行这些测试脚本。当前文件为js版本,模拟用户在DApp客户端用户界面操作的情形。

var MetaCoin = artifacts.require("./MetaCoin.sol"); // 这与1_initial_migration.js文件的头是一样的,引入了一个智能合约文件。

contract('MetaCoin', function(accounts) {
  it("should put 10000 MetaCoin in the first account", function() {
    return MetaCoin.deployed().then(function(instance) {
      return instance.getBalance.call(accounts[0]);
    }).then(function(balance) {
      assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account");
    });
  });
  it("should call a function that depends on a linked library", function() {
    var meta;
    var metaCoinBalance;
    var metaCoinEthBalance;

    return MetaCoin.deployed().then(function(instance) {
      meta = instance;
      return meta.getBalance.call(accounts[0]);
    }).then(function(outCoinBalance) {
      metaCoinBalance = outCoinBalance.toNumber();
      return meta.getBalanceInEth.call(accounts[0]);
    }).then(function(outCoinBalanceEth) {
      metaCoinEthBalance = outCoinBalanceEth.toNumber();
    }).then(function() {
      assert.equal(metaCoinEthBalance, 2 * metaCoinBalance, "Library function returned unexpected function, linkage may be broken");
    });
  });
  it("should send coin correctly", function() {
    var meta;

    // Get initial balances of first and second account.
    var account_one = accounts[0];
    var account_two = accounts[1];

    var account_one_starting_balance;
    var account_two_starting_balance;
    var account_one_ending_balance;
    var account_two_ending_balance;

    var amount = 10;

    return MetaCoin.deployed().then(function(instance) {
      meta = instance;
      return meta.getBalance.call(account_one);
    }).then(function(balance) {
      account_one_starting_balance = balance.toNumber();
      return meta.getBalance.call(account_two);
    }).then(function(balance) {
      account_two_starting_balance = balance.toNumber();
      return meta.sendCoin(account_two, amount, {from: account_one});
    }).then(function() {
      return meta.getBalance.call(account_one);
    }).then(function(balance) {
      account_one_ending_balance = balance.toNumber();
      return meta.getBalance.call(account_two);
    }).then(function(balance) {
      account_two_ending_balance = balance.toNumber();

      assert.equal(account_one_ending_balance, account_one_starting_balance - amount, "Amount wasn't correctly taken from the sender");
      assert.equal(account_two_ending_balance, account_two_starting_balance + amount, "Amount wasn't correctly sent to the receiver");
    });
  });
});

我们来分析一波这个truffle metacoin js版本的单元测试:

  1. 直接函数contract走起,第一个参数为智能合约名字,第二个参数为匿名内部函数
  2. 匿名函数传入了当前账户地址,函数体是单元测试集
  3. 每个单元测试是由关键字it函数来做,第一个参数传入单元测试的comments,第二个参数传入一个无参匿名函数
  4. 进到无参匿名函数的函数体内,就是正式的单元测试内容,可以定义自己的成员属性,通过调用truffle内部组件自动部署合约逐一测试,使用成员属性接收返回值,最后使用关键字assert来判断是否符合预期。具体业务不详细展开,可根据自己业务内容随意更改。

这是官方文档,详细说明如何使用JS来编写智能合约的单元测试

初始化文件解释9:TestMetacoin.sol

好下面来看看Solidity智能合约版本的单元测试。一般来讲,这种文件的命名规则是Test加待测智能合约的名字拼串组成。

pragma solidity ^0.4.2;

import "truffle/Assert.sol";
import "truffle/DeployedAddresses.sol";
import "../contracts/MetaCoin.sol";

contract TestMetacoin {

  function testInitialBalanceUsingDeployedContract() public {
    MetaCoin meta = MetaCoin(DeployedAddresses.MetaCoin());

    uint expected = 10000;

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
  }

  function testInitialBalanceWithNewMetaCoin() public {
    MetaCoin meta = new MetaCoin();

    uint expected = 10000;

    Assert.equal(meta.getBalance(tx.origin), expected, "Owner should have 10000 MetaCoin initially");
  }

}

继续分析:

  • 首先import了truffle的几个类库,用来支持我们接下来的测试内容。然后import了待测智能合约。
  • 建立单元测试智能合约,根据合约不同方法定义对应的test测试方法。
  • 方法体内部去调用待测智能合约的方法,传参接收返回值,然后使用关键字assert判断是否符合预期。

这是官方文档,详细说明如何使用Solidity来编写智能合约的单元测试

2.3 编译合约

键入

truffle compile

输出情况:

输出结果

根据编译输出的路径地址./build/contracts,我们去查看一下

产生文件列表

可以看到原来所在在contracts目录下的智能合约文件(有合约contract,有库library)均被编译成了json文件。

这些json文件就是truffle用来部署合约的编译文件。 ##2.4 配置以太坊本地环境 truffle.js是truffle的配置文件,启动好以太坊本地结点以后,我们需要让truffle去识别它并使用它,这就需要在truffle.js中配置相关属性:

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*" // Match any network id
    }
  }
};

【说明】如果不启动TestRPC,直接执行部署合约的话,会有以下错误提示:

无网络

2.5 启动本地以太坊客户端结点

启动适合开发的RPC客户端

启动之前安装好的EthereumJS RPC客户端。

testrpc

【说明】一定要启动一个新的客户端执行testrpc命令,可以观察到默认账户和私钥信息。

本地客户端

2.6 部署合约

移植(migrate),对这里叫移植,但下面我们仍使用“部署”这个词,truffle中部署的命令为:

truffle migrate

输出结果截图如下:

部署智能合约的输出结果

查看testrpc的输出窗口,可以看到这笔交易和花费的区块:

image.png

2.7 测试合约

我们知道在执行编译时会自动执行这些单元测试,如果有一个测试未通过则会中断编译过程。而在开发阶段,我们也可以自己使用命令来测试。

truffle test

没有报错就说明通过了,绿条“5 passing(2s)”,有报错就会打印在下方。

输出截图1
输出截图2

3. 用Truffle框架运行一个“Hello World!”智能合约

3.1 创建工程目录

返回父级目录,创建一个文件夹HelloWorld,来做为你的工程根目录。

mkdir HelloWorld

输入结果:

创建并进入该目录

3.2 初始化框架

在工作目录HelloWorld目录下,执行truffle初始化动作:

truffle init

输出截图:

初始化成功
采用SFTP下载文件到本地,可查看目录结构:

│  truffle-config.js
│  truffle.js
│  
├─contracts
│      Migrations.sol
│      
├─migrations
│      1_initial_migration.js
│      
└─test

目录结构简单说明如下:

contract/ - Truffle默认的合约文件存放地址。 migrations/ - 存放发布脚本文件 test/ - 用来测试应用和合约的测试文件 truffle.js - Truffle的配置文件

3.3 新建新合约

在./contract目录下创建一个自己的合约文件Greeter.sol。

pragma solidity ^0.4.17;

contract Greeter         
{
    address creator;     
    string greeting;     

    function Greeter(string _greeting) public   
    {
        creator = msg.sender;
        greeting = _greeting;
    }
    

    function greet() public constant returns (string)           
    {
        return greeting;
    }
    
    function setGreeting(string _newgreeting) public
    {
        greeting = _newgreeting;
    }
    
     /**********
     Standard kill() function to recover funds 
     **********/
    
    function kill()public
    { 
        if (msg.sender == creator)
            suicide(creator);  // kills this contract and sends remaining funds back to creator
    }

}

3.4 新建发布脚本

在./migrations/目录下新建一个文件:2_deploy_contracts.js,增加发布代码。

var Greeter = artifacts.require("./Greeter.sol");

module.exports = function(deployer) {
  deployer.deploy(Greeter,"Hello, World!");//"参数在第二个变量携带"
};

3.5 编译

进入到工程根目录./HelloWorld目录下,进行编译:

truffle compile

输出截图如下:

编译成功截图

3.6 启动你的客户端

如果之前没有启动RPC客户端的话,则需要启动之前安装好的EthereumJS RPC客户端。如果已启动的则忽略此步。

$ testrpc

3.7 部署合约(migrate)

执行部署命令(truffle migrate)提示出错。

truffle migrate

错误截图输出:

部署失败,提示网络未配置
修改文件./HelloWorld/truffle.js文件,增加网络配置:

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
     networks: {
        development: {
            host: "localhost",
            port: 8545,
            network_id: "*" // 匹配任何network id
         }
    }
};

重新执行编译命令,重新执行部署命令(truffle migrate),则运行正确。对应Greeter的智能合约地址为“0x7d62724f397a99613b84923a1166d683de2db680”

部署成功

3.8 TRUFFLE测试环境运行合约

Truffle提供了一种更加简单的方式,通过交互式控制台来与你的那些准备好的合约进行交互。 truffle console 一个基本的交互控制台,可以连接任何EVM客户端。如果你已经有了自己的ganache或者geth等EVM的本地环境,那么就可以使用truffle console来交互,所以如果你已经有一个现成的小组共享的开发用EVM,那么使用这个没错。 truffle develop 一个交互控制台,启动时会自动生成一个开发用区块链环境(其实我认为它与ganache就是一个底层实现机制,都是默认生成10个账户)。如果你没有自己的EVM环境的话,直接使用truffle develop非常方便。

truffle console

输入Greeter智能合约命令,显示打印出一个json结构,展示了它的各种属性内容。

查看Greeter结构
根据你的Greeter智能合约地址,运行Greeter智能合约命令:
hello,world智能合约运行成功

3.9 GETH正式环境运行合约

###启动GETH环境 本节假设GETH环境已安装好了。如果还没有安装的同学,可参考文章《第一课 如何在WINDOWS环境下搭建以太坊开发环境》(www.jianshu.com/p/683ea7d62…

然后在IDE内部打开一个terminal,启动GETH的EVM环境。

geth --datadir testNet3 --dev --rpc console

截图1
截图2

GETH 中是通过abi来注册合约对象的。 首先我们找到./build/contracts/Greeter.json中的abi的value:

"abi": [
    {
      "inputs": [
        {
          "name": "_greeting",
          "type": "string"
        }
      ],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "constructor"
    },
    {
      "constant": true,
      "inputs": [],
      "name": "greet",
      "outputs": [
        {
          "name": "",
          "type": "string"
        }
      ],
      "payable": false,
      "stateMutability": "view",
      "type": "function"
    },
    {
      "constant": false,
      "inputs": [
        {
          "name": "_newgreeting",
          "type": "string"
        }
      ],
      "name": "setGreeting",
      "outputs": [],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "function"
    },
    {
      "constant": false,
      "inputs": [],
      "name": "kill",
      "outputs": [],
      "payable": false,
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ],

通过json压缩成一行得到 var abi = [{"inputs": [{"name": "_greeting","type": "string"}],"payable": false,"stateMutability": "nonpayable","type": "constructor"},{"constant": true,"inputs": [],"name": "greet","outputs": [{"name": "","type": "string"}],"payable": false,"stateMutability": "view","type": "function"},{"constant": false,"inputs": [{"name": "_newgreeting","type": "string"}],"name": "setGreeting","outputs": [],"payable": false,"stateMutability": "nonpayable","type": "function"},{"constant": false,"inputs": [],"name": "kill","outputs": [],"payable": false,"stateMutability": "nonpayable","type": "function"}];

重新部署智能合约到Geth环境

启动一个新的命令窗口,到

cd /usr/work/HelloWorld truffle migrate

成功部署输出截图:

智能合约部署成功
获得Greeter的地址为 0xb52bb3ce336f71a14345c78e5b2f8e63685e3f92

切换到GETH环境下,利用api和智能合约地址(你自己Greeter智能合约的地址哦)注册合约对象。

var abi = [{"inputs": [{"name": "_greeting","type": "string"}],"payable": false,"stateMutability": "nonpayable","type": "constructor"},{"constant": true,"inputs": [],"name": "greet","outputs": [{"name": "","type": "string"}],"payable": false,"stateMutability": "view","type": "function"},{"constant": false,"inputs": [{"name": "_newgreeting","type": "string"}],"name": "setGreeting","outputs": [],"payable": false,"stateMutability": "nonpayable","type": "function"},{"constant": false,"inputs": [],"name": "kill","outputs": [],"payable": false,"stateMutability": "nonpayable","type": "function"}];
var HelloWorld = eth.contract(abi).at('0xb52bb3ce336f71a14345c78e5b2f8e63685e3f92')
HelloWorld.greet()

输出截图显示成功:

又是成功

4. 总结及参考

本文站在巨人的肩膀上,完成了以太坊开发框架Truffle从入门到实战的演示。对巨人的文章表示感谢: 1,Solidity的Truffle框架实战(手把手) 2, 【精解】开发一个智能合约 3,官网参考: truffleframework.com/docs/ 4, 官网GITHUB的代码: github.com/trufflesuit…

知识对接服务: 辉哥和欧阳哥哥在知识星球开通了区块链入门专栏,用于存放简书区块链入门专栏文章的工程源码等内容,并建立专项微信群用于技术交流,欢迎加入。