【本文目标】
经过本文的学习和时间,你将熟悉以太坊开发框架Truffle的配置和运行,并借助Truffle完成一个智能合约的部署。javascript
【技术收获】 经过本文的学习,你将掌握如下内容: 1,了解TRUFFLE的功能 2,了解TRUFFLE的安装,配置和启动 3,借助TRUFFLE完成METACOIN一个智能合约的运行 4,Testrpc,Geth环境的使用html
【实操课程列表】 第一课 如何在WINDOWS环境下搭建以太坊开发环境java
第二课 如何实现以太坊最简智能合约“Hello World”的运行node
第六课 技术小白如何开发一个DAPP区块链应用(以宠物商店为例)webpack
第七课 技术小白如何在45分钟内发行通证(TOKEN)并上线交易git
第八课 如何调试以太坊官网的智能合约众筹案例github
【说明】未列出的课程为知识普及的非实操类课程,全部区块链文章参考“区块链入口”专栏。web
Truffle是一个世界级的开发环境,测试框架,以太坊的资源管理通道,致力于让以太坊上的开发变得简单,Truffle有如下:npm
在Ubuntu命令上窗口输入如下命令,完成安装:
$ npm install -g truffle
若是安装成功,可输入truffle version名称,正常状况下会有版本显示:
NodeJS 5.0+ Windows,Linux(推荐Ubuntu),或Mac OS X
有许多的以太坊客户端能够选择。咱们推荐在开发和部署时使用不一样客户端。 适用开发的客户端
当开发基于Truffle的应用时,咱们推荐使用EthereumJS TestRPC。它是一个完整的在内存中的区块链仅仅存在于你开发的设备上。它在执行交易时是实时返回,而不等待默认的出块时间,这样你能够快速验证你新写的代码,当出现错误时,也能即时反馈给你。它同时仍是一个支持自动化测试的功能强大的客户端。Truffle充分利用它的特性,能将测试运行时间提速近90%。
适用正式发布的客户端
对此有许多官方和非官方的以太坊客户端可供选择。最好使用TestRPC客户端充分测试后,再使用这些客户端。这些是完整的客户端实现,包括挖矿,网络,块及交易的处理,Truffle能够在不须要额外配置的状况下发布到这些客户端。
当发布到私有网络中
私人网络中使用了相同的技术,但却有不一样的配置。因此你能够将上面说起的客户端来运行一个私有的网络,部署到这样的网络也是使用一样的方式。 【说明】做者使用TestRPC和 Geth (go-ethereum)这2种客户端,他们的安装方式参考文章:www.jianshu.com/p/683ea7d62…
咱们假设前面的安装和环境搭建已所有成功,此时应该能够直接使用命令truffle了,下面咱们创建一个工做间truffle-workspace,而后在工做间执行:
mkdir MetaCoin
cd MetaCoin
truffle unbox metacoin
复制代码
原来使用truffle init,但如今它存在于unbox。
执行截图以下:
Truffle 的盒子Boxs装有不少很是实用的项目样板,可让你忽略一些环境配置问题,从而能够集中与开发你本身的DApp的业务惟一性。除此以外,Truffle Boxes可以容纳其余有用的组件、Solidity合约或者库,先后端视图等等。全部这些都是一个完整的实例Dapp程序。均可如下载下来逐一研究,寻找适合本身公司目前业务模型的组件。
能够看到,如今官方盒子还很少,总共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…
进入metacoin目录,当前目录已经被初始化成一个新的空的以太坊工程,目录结构以下:
contracts * ConvertLib.sol * MetaCoin.sol * Migrations.sol * .placeholder migrations * 1_initial_migration.js * 2_deploy_contracts.js test * metacoin.js * TestMetacoin.sol * .placeholder
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具体的类型语法,咱们来分析一下这个文件:
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 onlyOwner是一个特别好的可读性极高的限制访问的手段。
那么restricted关键字如何使用呢?
好吧,我刚刚带着modifier的知识从新看了上面的Migrations合约的内容发现,restricted并非关键字,而是modifier的方法名,在其下的想增长该modifier功能的函数中,都使用了public restricted的方式来声明。
说到这里,我又明白了为何要使用public onlyOwner这种写法,由于public是函数可见性修饰符,onlyOwner是自定义的限制访问的modifier方法,他们都是关于函数使用限制方面的,因此会写在一块儿,能够假想一个括号将它俩括起来,他们占一个位置,就是原来属于public|private|internal|external的那个位置。
这一点很重要了,咱们研究一下Solidity自身携带的特殊变量以及函数:
msg有两个属性,一个是msg.sender,另外一个是msg.value,这两个值能够被任何external函数调用,包含库里面的函数。
注意谨慎使用block.timestamp, now and block.blockhash,由于他们都是有可能被篡改的。
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];
}
}
复制代码
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方法,如何操做呢,有两种办法:
第一种办法在方法自己比较耗时的状况下会阻塞,或者不会获取到准确的返回值。因此采用第二种办法:就是经过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消耗。
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源码也算功不可没。
var Migrations = artifacts.require("./Migrations.sol");
module.exports = function(deployer) {
deployer.deploy(Migrations);
};
复制代码
这个js文件是nodejs的写法,看上去它的做用就是部署了上面的Migrations智能合约文件。
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三个方法的详细用法,不难这里再也不赘述。
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
}
}
};
复制代码
这个例子展现了该配置文件能够配置网络环境,暂先到这,之后赶上了针对该配置文件进行研究。
This is a placeholder file to ensure the parent directory in the git repository. Feel free to remove.
翻译过来就是:placeholder文件是用来保证在git库中父级目录的,能够删除。
和下面的文件同样,他们的功能都是用来作单元测试的,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版本的单元测试:
这是官方文档,详细说明如何使用JS来编写智能合约的单元测试。
好下面来看看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");
}
}
复制代码
继续分析:
这是官方文档,详细说明如何使用Solidity来编写智能合约的单元测试。
键入
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,直接执行部署合约的话,会有如下错误提示:
启动以前安装好的EthereumJS RPC客户端。
testrpc
【说明】必定要启动一个新的客户端执行testrpc命令,能够观察到默认帐户和私钥信息。
移植(migrate),对这里叫移植,但下面咱们仍使用“部署”这个词,truffle中部署的命令为:
truffle migrate
输出结果截图以下:
查看testrpc的输出窗口,能够看到这笔交易和花费的区块:
咱们知道在执行编译时会自动执行这些单元测试,若是有一个测试未经过则会中断编译过程。而在开发阶段,咱们也能够本身使用命令来测试。
truffle test
没有报错就说明经过了,绿条“5 passing(2s)”,有报错就会打印在下方。
返回父级目录,建立一个文件夹HelloWorld,来作为你的工程根目录。
mkdir HelloWorld
输入结果:
在工做目录HelloWorld目录下,执行truffle初始化动做:
truffle init
输出截图:
│ truffle-config.js
│ truffle.js
│
├─contracts
│ Migrations.sol
│
├─migrations
│ 1_initial_migration.js
│
└─test
复制代码
目录结构简单说明以下:
contract/ - Truffle默认的合约文件存放地址。 migrations/ - 存放发布脚本文件 test/ - 用来测试应用和合约的测试文件 truffle.js - Truffle的配置文件
在./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
}
}
复制代码
在./migrations/目录下新建一个文件:2_deploy_contracts.js,增长发布代码。
var Greeter = artifacts.require("./Greeter.sol");
module.exports = function(deployer) {
deployer.deploy(Greeter,"Hello, World!");//"参数在第二个变量携带"
};
复制代码
进入到工程根目录./HelloWorld目录下,进行编译:
truffle compile
输出截图以下:
若是以前没有启动RPC客户端的话,则须要启动以前安装好的EthereumJS RPC客户端。若是已启动的则忽略此步。
$ testrpc
执行部署命令(truffle migrate)提示出错。
truffle migrate
错误截图输出:
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”
Truffle提供了一种更加简单的方式,经过交互式控制台来与你的那些准备好的合约进行交互。 truffle console 一个基本的交互控制台,能够链接任何EVM客户端。若是你已经有了本身的ganache或者geth等EVM的本地环境,那么就可使用truffle console来交互,因此若是你已经有一个现成的小组共享的开发用EVM,那么使用这个没错。 truffle develop 一个交互控制台,启动时会自动生成一个开发用区块链环境(其实我认为它与ganache就是一个底层实现机制,都是默认生成10个帐户)。若是你没有本身的EVM环境的话,直接使用truffle develop很是方便。
truffle console
输入Greeter智能合约命令,显示打印出一个json结构,展现了它的各类属性内容。
###启动GETH环境 本节假设GETH环境已安装好了。若是尚未安装的同窗,可参考文章《第一课 如何在WINDOWS环境下搭建以太坊开发环境》(www.jianshu.com/p/683ea7d62…
而后在IDE内部打开一个terminal,启动GETH的EVM环境。
geth --datadir testNet3 --dev --rpc console
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"}];
启动一个新的命令窗口,到
cd /usr/work/HelloWorld truffle migrate
成功部署输出截图:
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()
复制代码
输出截图显示成功:
本文站在巨人的肩膀上,完成了以太坊开发框架Truffle从入门到实战的演示。对巨人的文章表示感谢: 1,Solidity的Truffle框架实战(手把手) 2, 【精解】开发一个智能合约 3,官网参考: truffleframework.com/docs/ 4, 官网GITHUB的代码: github.com/trufflesuit…
知识对接服务: 辉哥和欧阳哥哥在知识星球开通了区块链入门专栏,用于存放简书区块链入门专栏文章的工程源码等内容,并创建专项微信群用于技术交流,欢迎加入。