以太坊Solidity之Truffle的使用流程与集成指南|猿创征文
Posted ╰つ栺尖篴夢ゞ
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了以太坊Solidity之Truffle的使用流程与集成指南|猿创征文相关的知识,希望对你有一定的参考价值。
一、Truffle 简介
① 什么是 Truffle ?
- Truffle 是一个世界级的开发环境,测试框架,以太坊的资源管理通道,致力于让以太坊上的开发变得简单。
- Truffle 有以下特性:
-
- 内置的智能合约编译,链接,部署和二进制文件的管理;
-
- 快速开发下的自动合约测试;
-
- 脚本化的,可扩展的部署与发布框架;
-
- 部署到不管多少的公网或私网的网络环境管理功能;
-
- 使用 EthPM&NPM 提供的包管理,使用 ERC190 标准;
-
- 与合约直接通信的直接交互控制台;
-
- 可配的构建流程,支持紧密集成;
-
- 在 Truffle 环境里支持执行外部的脚本。
② 环境要求和安装
- 环境要求:
-
- NodeJS 5.0+;
-
- Windows,Linux,或 Mac OS X。
- 安装方式:
$ npm install -g truffle
- Truffle 需要以太坊客户端,需要支持标准的 JSON RPC API。
- 如果你是 Windows 用户,推荐使用 Powershell 或 Git BASH 来安装和使用 Truffle 框架。
③ Truffle 开发工具
- 当开发基于 Truffle 的应用时,推荐使用 EthereumJS TestRPC,它是一个完整的在内存中的区块链仅仅存在于开发的设备上。它在执行交易时是实时返回,而不等待默认的出块时间,这样可以快速验证新写的代码,当出现错误时,也能即时反馈给你。它同时还是一个支持自动化测试的功能强大的客户端,Truffle 充分利用它的特性,能将测试运行时间提速近 90%。
- 使用 TestRPC 客户端充分测试后,可是尝试使用正式发布的客户端:Geth (go-ethereum)、WebThree(cpp-ethereum)、More,这些是完整的客户端实现,包括挖矿,网络,块及交易的处理,Truffle 可以在不需要额外配置的情况下发布到这些客户端。
二、Truffle 工程创建
- 创建工程目录,可以使用文件浏览器或使用下面的命令在命令行创建一个目录:
$ mkdir myproject
- 接下来,通过下面的命令初始化一个 Truffle 工程:
$ truffle
Truffle v3.4.11 - a development framework for Ethereum
$ cd myproject
$ truffle init
- 完成后,将拥有如下目录:
-
- app/ - 应用文件运行的默认目录,这里面包括推荐的 javascript 文件和 css 样式文件目录,但可以完全决定如何使用这些目录;
-
- contract/ - Truffle 默认的合约文件存放地址;
-
- migrations/ - 存放发布脚本文件 ;
-
- test/ - 用来测试应用和合约的测试文件;
-
- truffle.js - Truffle 的配置文件。
- truffle init 会默认创建一个构建在以太坊内的代币 demo 应用:METACOIN,可以使用这个工程来进行快速的学习,或者也可以删除这些文件来创建一个自己的工程。
三、编译合约
① 合约位置
- 所有合约应该位于 ./contracts 目录,默认提供一个合约文件,一个库文件,均以 .sol 结尾作为示例。
- 尽管库文件有一定的特殊性,但为简单起见,当前均称之为合约。
② 命令
- 要编译合约,使用:
truffle compile
- Truffle 仅默认编译自上次编译后被修改过的文件,来减少不必要的编译。如果想编译全部文件,可以使用 --compile-all 选项:
truffle compile --compile-all
③ 约定
- Truffle 需要定义的合约名称和文件名准确匹配。
- 举例来说,如果文件名为 MyContract.sol,那么合约文件须为如下两者之一:
contract MyContract
...
// or
library MyContract
...
- 这种匹配是区分大小写的,也就是说大小写也要一致,推荐大写每一个开头字母。
④ 依赖
- 可以通过使用 import 来声明依赖,Truffle 将会按正确顺序依次编译合约,并在需要的时候自动关联库。
⑤ 编译目录
- 编译的输出位于 ./build/contracts 目录,如果目录不存在会自动创建。
- 编译文件对于 Truffle 框架能否正常工作至关重要,不应该在正常的编译或发布以外手动修改这些文件。
四、移植
① 移植目的
- 移植是由一些 Javascript 文件组成来协助发布到以太坊网络,主要目的是用来缓存你的发布任务,它的存在基于你的发布需求会改变的前提。
- 当你的工程发生了重要的改变,你将创建新的移植脚本来将这些变化带到区块链上,之前运行移植的历史记录通过一个特殊的 Migrations 合约来记录到链上。
② 命令
- 执行移植,使用下述命令:
truffle migrate
- 如果你之前的移植是成功执行的,这个命令会执行所有的位于 migrations 目录内的移植脚本,truffle migrate 仅会执行新创建的移植。
- 如果没有新的移植脚本,这个命令不同执行任何操作,可以使用选项 --reset 来从头执行移植脚本。
③ 移植脚本文件
- 一个样例文件如下:文件名:4_example_migration.js
module.exports = function(deployer)
// deployment steps
deployer.deploy(MyContract);
;
- 需要注意的是文件名以数字开头,一个描述性的后缀结尾,数字前缀是必须的,用于记录移植是否成功,后缀仅是为了提高可读性,以方便理解。
- 移植 js 里的 exports 的函数接受一个 deployer 对象作为第一个参数,这个对象用于发布过程,提供了一个清晰的语法支持,同时提供一些通过的合约部署职责,比如保存发布的文件以备稍后使用。deployer 对象是用来缓存(stage)发布任务的主要操作接口。
- 像所有其它在 Truffle 中的代码一样,Truffle 提供了自己代码的合约抽象层(contract abstractions),并且进行了初始化,以方便可以便利的与以太坊的网络交互,这些抽象接口是发布流程的一部分。
④ 初始移植
- Truffle 需要一个移植合约来使用移植特性,这个合约内需要指定的接口,但可以按你的意味修改合约。对大多数工程来说,这个合约会在第一次移植时进行的第一次部署,后续都不会再更新。
- 通过 truffle init 创建一个全新工程时,获得一个默认的合约:文件名:contracts/Migration.sol:
contract Migrations
address public owner;
// A function with the signature `last_completed_migration()`, returning a uint, is required.
uint public last_completed_migration;
modifier restricted()
if (msg.sender == owner) _
function Migrations()
owner = msg.sender;
// A function with the signature `setCompleted(uint)` is required.
function setCompleted(uint completed) restricted
last_completed_migration = completed;
function upgrade(address new_address) restricted
Migrations upgraded = Migrations(new_address);
upgraded.setCompleted(last_completed_migration);
- 如果想使用移植特性,必须在第一次部署合约时,部署这个合约,可以使用如下方式来创建一次移植:文件名:migrations/1_initial_migrations.js:
module.exports = function(deployer)
// Deploy the Migrations contract as our only task
deployer.deploy(Migrations);
;
- 由此,可以接着创建递增的数字前缀来部署其它合约。
⑤ 部署器(deployer)
- 移植文件会使用部署器来缓存部署任务,因此可以按一定顺序排列发布任务,它们会按正确顺序执行:
// Stage deploying A before B
deployer.deploy(A);
deployer.deploy(B);
- 另一选中可选的部署方式是使用 Promise,将部署任务做成一个队列,是否部署依赖于前一个合约的执行情况:
// Deploy A, then deploy B, passing in A's newly deployed address
deployer.deploy(A).then(function()
return deployer.deploy(B, A.address);
);
- 如果想更清晰,也可以选择实现一个 Promise 链。
- 可以根据发布到的网络的具体情况进行不同的部署流程,要实现不同条件的不同部署步骤,移植代码中需要第二个参数 network。示例如下:
module.exports = function(deployer, network)
// Add demo data if we're not deploying to the live network.
if (network != "live")
deployer.exec("add_demo_data.js");
- 指定一个网络:大多数 Truffle 提供的命令根据指定的网络不同而表现不同,会使用对应网络下的合约和配置信息,可以通过 --network 选项在参数上进行控制:
$ truffle migrate --network live
networks:
development:
host: "localhost",
port: 8545,
network_id: "*" // match any network
,
live:
host: "178.25.19.88", // Random IP for example purposes (do not use)
port: 80,
network_id: 1, // Ethereum public network
// optional config values
// gas
// gasPrice
// from - default address to use for any transaction Truffle makes during migrations
- 在上面这个例子中,Truffle 会在 live 网络中进行移植,如果配置如上述配置示例的 Example 所指定的内容的话,是最终在以太坊网络上进行部署。
⑥ 部署 API
(A)DEPLOYER.DEPLOY(CONTRACT, ARGS…)
- 发布一个指定的合约,第一参数是合约对象,后面是一些可选的构造器参数。这个函数适用于单例合约,它只会在 dapp 中只创建一个这个合约的实例(单例),函数会在部署后设置合约的地址(如:C ontract.address 将等于新的部署地址),它将会覆盖之前存储的地址。也可以传入一个合约数组,或数组的数组来加速多合约的部署。
- 需要注意的是如果库的地址可用,deploy 会自动为这个部署的合约联接任何需要的库,因此如果合约依赖某个库,应该先部署这个库:
// Deploy a single contract without constructor arguments
deployer.deploy(A);
// Deploy a single contract with constructor arguments
deployer.deploy(A, arg1, arg2, ...);
// Deploy multiple contracts, some with arguments and some without.
// This is quicker than writing three `deployer.deploy()` statements as the deployer
// can perform the deployment as a batched request.
deployer.deploy([
[A, arg1, arg2, ...],
B,
[C, arg1]
]);
(B)DEPLOYER.LINK(LIBRARY, DESTINATIONS)
- 联接一个已经发布的库到一个或多个合约,destinations 可以是一个合约或多个合约组成的一个数组,如果目标合约并不依赖这个库,部署器会忽略掉这个合约。
- 这对于在 dapp 中不打算部署的合约(如:非单例)但却需要在使用前先联接的情况下非常有用。
// Deploy library LibA, then link LibA to contract B
deployer.deploy(LibA);
deployer.link(LibA, B);
// Link LibA to many contracts
deployer.link(LibA, [B, C, D]);
(C)DEPLOYER.AUTOLINK(CONTRACT)
- 关联合约依赖的所有库,这需要所依赖的库已经部署,或在其前一步部署:
// Assume A depends on a LibB and LibC
deployer.deploy([LibB, LibC]);
deployer.autolink(A);
- 另外可以省略参数来调用函数 autolink(),这会自动关联合约依赖的所有库,需要保证在调用这个函数前,所有被需要的库已经部署:
// Link *all* libraries to all available contracts
deployer.autolink();
(D)DEPLOYER.THEN(FUNCTION() …)
- Promise 语法糖,执行做生意的部署流程:
deployer.then(function()
// Create a new version of A
return A.new();
).then(function(instance)
// Set the new instance of A's address on B.
var b = B.deployed();
return b.setA(instance.address);
);
(E)DEPLOYER.EXEC(PATHTOFILE)
- 执行 truffle exec 做为部署的一部分:
// Run the script, relative to the migrations file.
deployer.exec("../path/to/file/demo_data.js");
五、构建应用
① 默认构建
- Truffle 集成了默认的构建来方便使用,但也许不适合每个项目,所以也许需要其它的来打包应用。默认的构造目标是 web 应用,但也可以很容易的转变为其它的构造流程,比如适用于命令行或库的流程。
- 默认构建有一些特性快速的开始:
-
- 在浏览器内自动的初始化你的应用,包括引入你编译的合约,部署的合约信息,和以太坊客户端信息配置;
-
- 包含常见的依赖,如 web3 和 Ether Pudding;
-
- 内置支持 ES6 和 JSX;
-
- SASS 支持;
-
- Uglifyjs 支持。
② 配置
- 可以随间的修改默认的构建内容,原始的构建内容目录如下:
app/
- javascripts/
- app.js
- stylesheets/
- app.css
- images/
- index.html
- 构建配置文件如下:
"build":
// Copy ./app/index.html (right hand side) to ./build/index.html (left hand side).
"index.html": "index.html",
// Process all files in the array, concatenating them together
// to create a resultant app.js
"app.js": [
"javascripts/app.js"
],
// Process all files in the array, concatenating them together
// to create a resultant app.css
"app.css": [
"stylesheets/app.scss"
],
// Copy over the whole directory to the build destination.
"images/": "images/"
- 配置文件中的配置键描述了最终的打包目标名称,右边的配置目录或文件数组则是要打包的目录的内容。打包过程根据文件扩展,将文件连接形成一个结果文件,并放到构建的目标位置。如果指定的是一个字符串而不是一个数组,这个字符串代指的文件如果需要会直接拷到对应的构建目录。如果字符串以“/”结尾,则会被识别为一个目录,整个目录会不经调整直接拷贝到对应的目录,所以的指定值都是默认相对于 /app 目录来指定的。
- 可以在任何时间改变配置和目录结构,并不强制要求需要 javascript 和 css 文件目录,所以删除构建配置文件中的对应配置就可以
- 特别注意:如果想默认构建在前端初始化你的应用,务必保证有一个构造目标 app.js,因为默认构建会将相关代码附加到这个文件,而不是其它文件。
③ 命令
- 要创建前端工程,执行:
truffle build
④ 构建结果
- 构建结果存在 ./build 目录,所以合约文件则在对应的位置 ./build/contracts。
- 默认构建虽简单易用,但它仍有一些缺点:
-
- 当前不支持 import,require 等,不能提供 browserify,Webpack 和 CommonJS 这样的工具,由此让依赖管理变得有些困难;
-
- 这是一套自定义的构建系统,与其它流行构建系统不兼容;
-
- 它可以扩展,但是自定义的方法和 API。
六、合约交互
① 读写数据
- 标准的与以太坊网络交互的方法是通过以太坊官方构建的 Web3 库。尽管这个库非常有用,但使用其提供接口与合约交互有些困难,特别是以太坊的新手。为降低学习曲线,Truffle 使用 Ether Pudding 库,它也是基于 Web3 的基础之上,目的是为了让交互更简单。
- 以太坊网络把在网络上读与写数据进行了区分,这个区分对于如何写程序影响很大。通常来说,写数据被称作交易(transaction),读数据被称作调用(call)。
- 对于交易与调用,它们分别有如下特性:
-
- 交易(Transaction):交易本质上改变了整个以太坊网络的数据状态,一个交易可以是向另一个帐户发送ether(以太坊网络代币)这样的简单行为,也可以是执行合约函数,添加一个新合约到以太坊网络这样的复杂行为。交易的典型特征是写入(或修改)数据,交易需要花费 ether,也被称作 gas,交易的执行需要时间。当你通过交易执行一个合约的函数时,你并不能立即得到执行结果,因为交易并不是立即执行的。大多娄情况下,通过执行交易不会返回值;它会返回一个交易的 ID,总的来说,交易具有如下特征:
-
-
- 需要gas(Ether)
-
-
-
- 改变网络的状态
-
-
-
- 不会立即执行
-
-
-
- 不会暴露返回结果(仅有交易 ID)
-
-
- 调用:则与上述的交易非常不同,调用可以在网络上执行代码,但没有数据会被改变(也许仅仅是些临时变量被改变),调用的执行是免费的,典型的行为就是读取数据。通过调用执行一个合约函数,你会立即得到结果。总的来说,调用具有如下特征:
-
-
- 免费(不花费 gas)
-
-
-
- 不改变网络状态
-
-
-
- 立即执行
-
-
-
- 有返回结果。
-
- 如果选择,取决于你想干什么,或者说想写数据,还是读数据。
② 接口(abstract)
- 为了来体验一下合约接口的作用,使用框架自带的默认 metacoin 的合约:
import "ConvertLib.sol";
contract MetaCoin
mapping (address => uint) balances;
event Transfer(address indexed _from, address indexed _to, uint256 _value);
function MetaCoin()
balances[tx.origin] = 10000;
function sendCoin(address receiver, uint amount) returns(bool sufficient)
if (balances[msg.sender] < amount) return false;
balances[msg.sender] -= amount;
balances[receiver] += amount;
Transfer(msg.sender, receiver, amount);
return true;
function getBalanceInEth(address addr) returns(uint)
return ConvertLib.convert(getBalance(addr),2);
function getBalance(address addr) returns(uint)
return balances[addr];
- 合约有三个方法和一个构造方法,三个方法可以被执行为交易或调用。现在来看看 Truffle 和 Ether Pudding 提供的叫 MetaCoin 的 Javascript 对象,可以在前端中使用:
// Print the deployed version of MetaCoin
console.log(MetaCoin.deployed());
// outputs:
//
// Contract
// - address: "0xa9f441a487754e6b27ba044a5a8eb2eec77f6b92"
// - allEvents: ()
// - getBalance: ()
// - getBalanceInEth: ()
// - sendCoin: ()
- 接口层提供了合约中以应的函数名,它还包含一个地址,指向到 MetaCoin 合约的部署版本。
③ 执行合约函数
(A)执行交易
- 在上述例子 MetaCoin 合约中,有三个可以执行的函数,如果对这三个函数稍加分析就会发现,只有 sendCoin 会对网络造成更改,sendCoin 函数的目标将 Meta Coin 从一个帐户发送到另一些帐户,这些更改需要被永久存下来。
- 当调用 sendCoin,我们将把它们作为一个交易来执行。如下所示,来演示下把 10 个币,从一个帐户发到另一个帐户,改变要永久的保存在网络上:
var account_one = "0x1234..."; // an address
var account_two = "0xabcd..."; // another address
var meta = MetaCoin.deployed();
meta.sendCoin(account_two, 10, from: account_one).then(function(tx_id)
// If this callback is called, the transaction was successfully processed.
// Note that Ether Pudding takes care of watching the network and triggering
// this callback.
alert("Transaction successful!")
).catch(function(e)
// There was an error! Handle it.
)
- 分析:
-
- 直接调用接口的 sendCoin 函数,最终是默认以交易的方式来执行的;
-
- 交易被成功执行时,回调函数会直到交易被执行时才真正被触发,这样带来的一个好处是不用一直去检查交易的状态;
-
- 对 sendCoin 函数传递了第三个参数,需要注意的是原始合约函数的定义中并没有第三个参数,这里是一个特殊的对象,用于编辑一些交易中的指定细节,它可以总是做为第三个参数传进,设置 from 的地址为 account_one。
(B)执行调用
- 继续用 MetaCoin 的例子,其中的 getBalance 函数就是一个很好的从网络中读取数据的例子,它压根不需要进行任何数据上的变更,它只是返回传入的地址的帐户余额,来简单看一下:
var account_one = "0x1234..."; // an address
var meta = MetaCoin.deployed();
meta.getBalance.call(account_one, from: account_one).then(function(balance)
// If this callback is called, the call was successfully executed.
// Note that this returns immediately without any waiting.
// Let's print the return value.
console.log(balance.toNumber());
).catch(function(e)
// There was an error! Handle it.
)
- 分析:
-
- 必须通过 .call() 来显示的向以太坊网络表明,并不会持久化一些数据变化;
-
- 得到返回结果,而不是一个交易 ID,需要注意的是,以太坊网网络可以处理非常大的数字,我们被返回一个 BigNumber 对象,框架再将这个对象转化了一个 number 类型。
- 在上述的例子中将返回值转成了一个 number 类型,是因为例子中的返回值比较小,如果将一个 BigNumber 转换为比 javascript 支持的 number 最大整数都大,将会出现错误或不可预期的行为。
(C)捕捉事件(Catching Events)
- 合约可以触发事件,可以进行捕捉以进行更多的控制,事件 API 与 Web3 一样:
var meta = MetaCoin.deployed();
var transfers = meta.Transfer(fromBlock: "latest");
transfers.watch(function(error, result)
// This will catch all Transfer events, regardless of how they originated.
if (error == null)
console.log(result.args);
- METHOD:DEPLOYED():每一个抽象出来的合约接口都有一个 deployed() 方法,上述例子中,调用这个函数返回一个实例,这个实例代表的是之前部署到网络的合约所对应的抽象接口的实例:
var meta = MetaCoin.deployed();
- 这仅对使用 truffle deploy 部署的合约,且一定是在 project configuration 中配置发布的才有效,如果不是这样,这个函数执行时会抛出异常。
- METHOD:AT():类似于 deployed(),可以通过一个地址来得到一个代表合约的抽象接口实例,当然这个地址一定是这个合约的部署地址:
var meta = MetaCoin.at("0x1234...")
- 当你的地址不正确,或地址对应的合约不正确时,这个函数并不会抛出异常,但调用接口时会报错,请保证在使用 at() 时输入正确的地址。
- METHOD:NEW():可以通过这个方法来部署一个完全全新的合约到网络中:
MetaCoin.new().then(function(instance)
// `instance` is a new instance of the abstraction.
// If this callback is called, the deployment was successful.
console.log(instance.address);
).catch(function(e)
// There was an error! Handle it.
);
- 需要注意的是这是一个交易,会改变网络的状态。
七、测试合约
① 框架
- Truffle 使用 Mocha 测试框架来做自动化测试,使用 Chai 来做断言,这两个库的结合可能让人耳目一新,基于这两者之上,提供一种方式来编译简单和可管理的合约自动化测试用例。
- 测试文件应置于 ./tests 目录,Truffle 只会运行以 .js,.es,.es6 和 .jsx 结尾的测试文件,其它的都会被忽略。
② 测试用例
- 每个测试文件至少应该包含至少一个对 Mocha 的 describe() 函数的调用,另一种方式是使用 Truffle 自定义的 contract() 函数,作用类型 describe() 函数,但额外添加了一些特性:
-
- 在每一个 contract() 函数执行前,合约都会重部署到以太坊客户端中,这样测试用例会在一个干净状态下执行;
-
- contract() 函数支持传入多个可用的帐户做为第二个参数传入,可以用此来进行测试。
- 当需要与所写的合约进行交互时,可以使用 contract(),否则使用 describe() 函数。
③ 测试用例示例
- truffle init 命令提供一个简单的测试用例例子,它会先部署你的合约,然后执行在 it() 块中指定的测试用例:
contract('MetaCoin', function(accounts)
it("should put 10000 MetaCoin in the first account", function()
// Get a reference to the deployed MetaCoin contract, as a JS object.
var meta = MetaCoin.deployed();
// Get the MetaCoin balance of the first account and assert that it's 10000.
return meta.getBalance.call(accounts[0]).then(function(balance)
assert.equal(balance.valueOf(), 10000, "10000 wasn't in the first account");
);
);
);
- 需要注意的是在 contract() 函数的传入的 MetaCoin 仅仅因为用来展示,说明它是 MetaCoin 相关的测试,并无本质作用。
④ 合约
- Truffle 提供了接口抽象,方便与合约进行便捷交互,通过 var meta = MetaCoin.deployed() 这行,Truffl e设法保证可以在测试用例、前端、移植(Migration)中都能用这种方式与你自己写的合约进行交互。
⑤ 命令
- 要执行测试,执行下面的命令:
truffle test
- 也可以对单个文件执行测试:
truffle test ./path/to/test/file.js
八、控制台
- 有时在进行测试和 debug 时,或手动执行交易时与合约进行直接交互是需要的。Truffle 提供了一种更加简单的方式,通过交互式控制台来与你的那些准备好的合约进行交互。
- 启动控制台,使用:
truffle console
- 这会使用默认网络来调起一个控制台,会自动连接到一个运行中的以太坊客户端,可以使用选项 --network 来修改这个特性。
- 当你加载了控制台,会看到下面的输出,default 是当前连接到的是默认网络:
$ truffle console
truffle(default)>
- 控制台支持 Truffle 命令行支持的命令,比如可以在控制台中执行 migrate --reset,其效果与在命令行中执行 truffle migrate --reset 的效果一致。
- Truffle 的控制台额外增加如下特性:
-
- 所有已经编译的合约都可用,就像在开发测试,前端代码中,或者移植代码中那样使用;
-
- 在每个命令后,合约会被重新加载,如使用 migrate --reset 命令后,可以立即使用新分配的地址和二进制;
-
- web3 库也可以使用,且也连到以太坊客户端;
-
- 所有命令返回的 promise,会自动解析,直接打印出结果,可以不用输入 then(),简化了命令。如下:
truffle(default)> MyContract.deployed().getValue.call();
九、外部脚本
- 也许会经常的执行外部脚本来与合约进行交互,Truffle 提供一个简单的方式来进行。
- 首先配置文件是 truffle.js,位于项目的根目录下,这个文件是 Javascript 文件,支持执行代码来创建配置,它必须导出一个对象,来代表项目配置。
- 当使用 Windows 的命令行时,默认的配置文件名与 truffle 冲突,这种情况下,推荐使用 Windows 的 power Shell 或 Git BASH,也可以将配置文件重命名为 truffle-config.js 来避免冲突:
module.exports =
build:
"index.html": "index.html",
"app.js": [
"javascripts/app.js"
],
"app.css": [
"stylesheets/app.css"
],
"images/": "images/"
,
rpc:
host: "localhost",
port: 8545
以太坊是什么,智能合约,编程语言:Solidity,DApp: 去中心化的应用程序,Truffle
以太坊 DApp 开发入门实战! 用Node.js和truffle框架搭建——区块链投票系统!