部署合约

部署合约是通过 JS 脚本实现的, 用于将合约部署到 Ethereum 网络中. 部署文件内编写的是部署的全部过程, 并且是默认支持部署需求的版本更新的. 随着项目的进展, 可以创建新的部署脚本, 用于新的区块链部署需求. 历史的部署已经通过 Migrations 合约上链记录了.

 命令

执行合约的部署:

$ truffle migrate

该命令将会运行 migrations/ 文件夹下全部的合约脚本. 合约脚本就是一组开发过程中的脚本文件. 如果合约不是首次执行, 该命令将从上一次成功执行的部署后执行, 如果没有新的部署, 该命令不会执行任何动作. 可以使用 --reset 参数来从头重新执行所有合约脚本, 本地测试时确保安装并且启动了测试区块链程序(例如 Ganache )

 部署文件

部署文件大致如下:

文件名: 4_example_migration.js

var MyContract = artifacts.require("MyContract");

module.exports = function(deployer) {
  // deployment steps
  deployer.deploy(MyContract);
};

注意: 文件名以数字开头, 以下划线与其他部分名字间隔. 开头的数字是部署的顺序, 也是合约有没有部署的标记. 后面的内容是用于标记部署脚本的内容的, 部署哪些合约.

 引入工件 artifacts.require()

在部署脚本开始, 我们需要通过 artifacts.require() 来告诉 Truffle 我们在这个工件中需要与哪些合约交互. 这个方法与 NodeJS 的 require 非常类似, 只是我们返回的是合约的概念, 用于后面的部署. 引入时不像NodeJS引入的是文件名, 而是引入合约的类名, 因为一个文件中可能会有多个类.

例如下面这个有两个类的合约文件:

文件名: ./contracts/Contracts.sol


contract ContractOne {
  // ...
}

contract ContractTwo {
  // ...
}

如果只需要 ContractTwo, 那么 artifacts.require() 语句就要这么写:

var ContractTwo = artifacts.require("ContractTwo");

要使用两个合约, 需要调用 artifacts.require() 两次:

var ContractOne = artifacts.require("ContractOne");
var ContractTwo = artifacts.require("ContractTwo");

 module.exports

所有部署脚本必须使用 module.exports 导出. 导出的部署脚本需要可以接收一个 deployer 对象作为第一个参数. 该对象不仅致力于为合约部署提供一个简单的语法, 还希望提供一些其他开发需要的功能, 例如保存部署工件为后续使用. deployer 对象是我们的筹划开发计划的核心对象, API参考本文后面内容.

部署脚本的方法可以接受其他参数, 参考下面例子.

 初始化脚本

Truffle 需要一个 Migrations 合约, 以便于管理其他部署脚本. 这个合约需要包含一个特定的接口, 此外可以随需求更改. 对于大部分项目来说, Migrations合约将被当做第一个合约进行部署, 此后将无需更新. 使用 truffle init 初始化项目的时候, 也会自动生成这个合约.

文件名: contracts/Migrations.sol

pragma solidity ^0.4.8;

contract Migrations {
  // 标记合约的拥有者
  address public owner;

  // 该变量自动生成 `last_completed_migration()` 方法, 返回一个uint, 是必须的属性.
  uint public last_completed_migration;

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

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

  // `setCompleted(uint)` 方法是必须方法.
  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_migration.js

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

module.exports = function(deployer) {
  // Deploy the Migrations contract as our only task
  deployer.deploy(Migrations);
};

至此之后, 可以创建基于这个序号的新的部署脚本, 用于部署其他合约, 执行新的开发步骤.

 Deployer

你的部署脚本文件需要使用 deployer 来筹划开发计划的步骤. 你可以开始编写同步的开发计划, 他们将按照合适的顺序进行.

// 步骤A在步骤B之前
deployer.deploy(A);
deployer.deploy(B);

此外, deployer 中的方法也都可以当做 Promise 来使用, 用于排队等待前面依赖的任务完成.

// 先部署A, 之后再部署B, 将新部署A的地址传给B
deployer.deploy(A).then(function() {
  return deployer.deploy(B, A.address);
});

如果部署关系并不复杂, 可以将全部部署内容写到一个 promise 队列中, 不必分文件. API参考下面内容.

 网络配置考量

可以根据部署网络目标的不同, 采取不同的部署步骤. 这是一个高级功能, 参考 网络 部分后再继续阅读本部分内容.

如果需要根据不同条件规划步骤, 需要将使用网络的名称作为第二参数传入:

module.exports = function(deployer, network) {
  if (network == "live") {
    // 当连接 "live" 网络的时候进行的操作.
  } else {
    // 执行其他操作.
  }
}

 使用的以太坊的账户

合约部署也会传入 Ethereum 客户端和 web3 提供的账户列表作为第三个参数. 参数列表与 web3.eth.getAccounts() 返回的参数列表顺序一致.

module.exports = function(deployer, network, accounts) {
  // 使用账户列表
}

 API 接口

deployer 为了简化部署脚本, 内置了许多方法.

 deployer.deploy(contract, args…, options)

部署一个指定的合约, 合约作为第一个参数(内容是使用 artifacts.require() 导入的 ContractName ), 其余参数可选. 本方法对单例合约非常有用, 可以在 dapp 中保证只有一个合约对象的单例. 将在合约部署之后设置好合约的地址(并且赋值给 ContractName.address ). 并且会覆盖前面设置好的合约地址.

也可以传入合约列表, 来加速多个合约的部署. 此外最后一个参数是选项对象, 包含 overwrite 和一些其他交易相关字段, 例如 gasfrom, 这些都是可选的. 如果 overwrite 参数设置为 false, 那么将不会在合约已经部署过之后重新部署合约. 这在使用使用外部依赖合约的时候非常有用.

注意: 需要在执行 deploy 之前确认所有依赖的内容(其他合约和库)都已经部署过, 参考下面的 link 方法了解详细内容.

参考 truffle-contract 文档了解更多内容.

Examples:

// 部署一个独立的合约
deployer.deploy(A);

// 部署一个独立合约, 给构造方法传参
deployer.deploy(A, arg1, arg2, ...);

// 合约已部署过就不再重新部署
deployer.deploy(A, {overwrite: false});

// 设置 gas 最大值, 以及部署使用的账号
deployer.deploy(A, {gas: 4612388, from: "0x...."});

// 部署多个合约, 一些需要参数, 一些不需要. 这比使用三个单独的 `deployer.deploy()` 要快, 因为只需要一次请求.
deployer.deploy([
  [A, arg1, arg2, ...],
  B,
  [C, arg1]
]);

// 外部依赖例子
//
// 本例中使用第三方合约, 该合约在正式环境下有相应的实例了, 但是在其他环境下 `test`, `dev` 并没有.
// 当我们在正式环境下使用该部署过的地址, 当我们在测试环境下进行创建后使用新创建的地址. 
// 与其编写 network 判断, 不如直接指定 `overwrite` 参数.
deployer.deploy(SomeDependency, {overwrite: false});

 deployer.link(library, destinations)

将一个已经部署过的库连接到一个或者多个合约之中. destinations 可以是一个单独的合约, 也可以是合约的列表. 如果目标合约并不需要该库, 那么将跳过该合约.

Example:

// 部署 LibA 库, 然后将 LibA 库连接到 合约B, 然后部署 合约B.
deployer.deploy(LibA);
deployer.link(LibA, B);
deployer.deploy(B);

// 将 LibA 库连接到多个合约
deployer.link(LibA, [B, C, D]);

 deployer.then(function() {…})

Use this to call specific contract functions during your migration to add, edit and reorganize contract data.

例子:

var a, b;
deployer.then(function() {
  // 创建一个新的 A 的合约
  return A.new();
}).then(function(instance) {
  a = instance;
  // 获取已经部署好的 B 的合约
  return B.deployed();
}).then(function(instance) {
  b = instance;
  // 使用B的 setA 方法, 将A的地址存储到B中.
  return b.setA(a.address);
});