详解 Truffle Migrations(迁移)- 合约部署不再困惑

通过本文可以学习到:

  1. Truffle 的迁移合约 Migrations.sol 有啥作用?

  2. 多个合约如何一起部署;

  3. 如果针对不同网络、不同账号进行条件部署;

  4. 如何链接库。

Migrations(迁移)是开发人员使数据及其支持结构的一套自动化部署的方法, 它们对于管理新软件版本的部署非常有用,因此并非仅限于区块链开发。

Truffle migrations 可以让我们把合约推到以太坊链上(不管是本地网络、测试网络还是主网),以及与其他合约链接或使用初始化合约数据。

migrations(迁移)真正最赞的地方是对区块链上合同地址的管理。 Truffle 几乎完全抽象化这项乏味的工作。

准备

确保安装好了 Truffle Framework  Ganache CLI.

开始

新建(或选择)一个项目目录,然后运行 truffle init ,会有类似下面的输出:

Downloading...Unpacking...Setting up...Unbox successful. Sweet!Commands:

  Compile:        truffle compile
  Migrate:        truffle migrate
  Test contracts: truffle test

init 命令在当前目录中初始化了一个 Truffle 项目。 目录看起来像这样:

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

 contracts 目录下, 创建一个合约文件: Storage.sol 代码如下:

pragma solidity > 0.4.21;

contract Storage {

    mapping (string => string) private _store;    function addData(string key, string value) public {        require(bytes(_store[key]).length == 0);
        _store[key] = value;
    }    function removeData(string key) public returns (string) {        require(bytes(_store[key]).length != 0);        string prev = _store[key];        delete _store[key];        return prev;
    }    function changeData(string key, string newValue) public {        require(bytes(_store[key]).length != 0);
        _store[key] = newValue;
    }

}

初始的迁移(Migrations)文件及部署规则

你可以能已经看到了在运行 truffle init 时生成的 Migrations.sol  1_initial_migration.js 

初始迁移合约一般不需要修改,他们是跟踪部署在区块链上的地址。当然也可以按照自己的需要修改 Migrations.sol 合约文件,进行一些高级的迁移管理,但需要保留 truffle init 命令创建的接口。

1_initial_migration.js 迁移文件,仅仅是说明如何把  Migrations.sol 合约部署到对应的链上。

1_initial_migration.js 迁移文件名,前面的序号,代表着 truffle migrate 运行迁移文件的顺序,1 表示第一个运行的迁移文件(从 1 开始)。 我们可以创建其他的迁移文件: 2_mycontract_migration.js ,在每个合约部署完成,Truffle 会把迁移序号保存到 Migrations 合约的 last_completed_migration

假设 migrations 就这两个迁移文件,truffle migrate 运行时实际会发生 4 笔交易:

  1. 运行 1_initial_migration.js 进行部署

  2. 把序号 1 写入到合约 Migrations

  3. 运行 2_mycontract_migration.js 进行部署

  4. 把序号 2 写入到合约 Migrations

last_completed_migration 表示的是最后部署的迁移,之后再加入其它的迁移文件:3_yourcontract.js 时, 运行 truffle migrate 时 Truffle 会首先读取 last_completed_migration 状态变量,参看之前部署到了哪些,在部署比 last_completed_migration 序号大的(所有)迁移文件,这样就可以保证不会重复部署。

注意,如果修改一个已有的合约,需要重新部署的话,直接运行 truffle migrate 是不会自动部署的,需要新加(或修改)一个更高序号的迁移文件,再运行 truffle migrate

truffle migrate 可以接一个 -f 序号 来强制重从一个需要开始执行迁移(此时会忽略 last_completed_migration 的值)。 例如: truffle migrate -f 2 会从第 2 个迁移文件开始部署。

迁移的相关数据

为了将智能合约部署到以太坊区块链,首先需要编写迁移文件,在 migrations 目录下,创建一个迁移文件  2_deploy_contracts.js, 现在项目的结构如下:

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

使用 migrations 部署合约,需要读取合约的 artifacts(构建)。构建文件会描述合同部署的网络及地址以及合约所包含的函数等。

而 artifacts(构建) 的数据从哪来呢?

在项目目录下,运行  truffle compile, 如果没有错误的话,会有下面的输出:

Compiling ./contracts/Migrations.sol...
Compiling ./contracts/Storage.sol...

Writing artifacts to ./build/contracts

或许在不同的编译器版本下,可以会有一些警告,只要没有错误就 OK 。

再次查看项目的目录结构,会多出一个 build 目录,如下:

.
├── build│   └── contracts│       ├── Migrations.json│       └── Storage.json├── contracts│   ├── Migrations.sol│   └── Storage.sol├── migrations│   ├── 1_initial_migration.js│   └── 2_deploy_contracts.js├── test├── truffle-config.js└── truffle.js

build 目录包含两个文件:Migrations.json  Storage.json 他们对应着 contracts 目录下的两个合约文件。

*.json 文件描述了他们对应的合约,包含:

  • 合约名称

  • 合约 ABI (应用二进制接口 — 描述合约的所有的方法及其对应的参数及返回值)

  • 合约字节码 bytecode (编译后的合约数据)

  • 合约部署的 bytecode (已经部署在链上的最新的 bytecode)

  • 合约最后编译使用的编译器版本

  • 已部署合约的网络列表,以及每个网络上的合约地址。

这个文件使 Truffle 能够创建一个 JavaScript 包装器(即 truffle-contract)来与智能合约通信。例如,当在 JavaScript 代码中调用 contract.address 时,Truffle 框架从 *.json 文件读取地址,并让我们方便的在不同的合约版本和网络之间进行转换。

编写迁移文件

有了这些知识,让我们写下我们的第一个迁移文件 2_deploy_contracts.js, 代码如下:

// 获取对应的合约文件var Storage = artifacts.require("./Storage.sol");// JavaScript exportmodule.exports = function(deployer) {    // deployer 是用来部署

    // 部署
    deployer.deploy(Storage);
}

编写迁移就这么简单。 为了运行迁移脚本,请在终端中运行以下命令:

truffle migrate

不过这时候,会得到一个错误:

Error: No network specified. Cannot determine current network.

意思是 Truffle 找不到要部署到的网络,这时可以在命令行终端打开一个新的 tab 运行 ganache-cli来启动一个模拟的区块链,启动后,有会类似输出:

Ganache CLI v6.9.0 (ganache-core: 2.10.0)Available Accounts==================(0) 0x828da2e7b47f9480838f2077d470d39906ad1d8e(1) 0xa4928865329324560185f1c93b5ebafd7ae6c9e8(2) 0x957b8b855bed52e11b2d7e9b3e6427771f299f3f(3) 0xf4b6bcabedaf1ccb3d0c89197c4b961460f1f63d...Private Keys==================(0) 8729d0f1d876d692f2f454f564042bd11c1e6d0c9b1808954f171f6f7b926fd6(1) 452dfeee16e5a0e34fa5348f0ef11f39a8b4635e5f454f77fc228ca9598f6997(2) 9196ad9fd6234f09ee13726cb889dcbc438c15f98e8ff1feb36a93758fa6d10a(3) fa47edd832e896314544b98d7e297ac2ce2097b49f8a9d7e7ae0e38154db8760....HD Wallet==================Mnemonic:      void august badge future common warfxlb ...Base HD Path:  m/44'/60'/0'/0/{account_index}Listening on localhost:8545

现在已经建立了一个私有区块链,它运行在 localhost:8545 上。现在让我们配置 Truffle 以便部署合约到该网络。

打开 truffle-config.js 文件,加入以下内容:

module.exports = {
  networks: {
    development: {
      host: "127.0.0.1",
      port: 8545,
      network_id: "*"
    }
  }};

上面配置的含义是把编译的合约部署到 localhost:8545 所在的网络上。

现在运行 truffle migrate , 得到以下输出:

Using network 'development'.Running migration: 1_initial_migration.js
  Deploying Migrations...
  ... 0x06595c0eccde8cb0cf642df07beefea11e3e96bfb470e8tinyxiong6567cecc37aed8
  Migrations: 0x6008e9a2c213d51093d0f18536d1aa3b00a7e058Saving successful migration to network...
  ... 0x392fb34c755970d1044dc83c56df6e51d5c4d4011319f659026ba27884126d7bSaving artifacts...Running migration: 2_deploy_contracts.js
  Deploying Storage...
  ... 0xb8ec575a9f3eca4a11a3f61170231a1816f7c68940d8487e56567adcf5c0a21e
  Storage: 0xd8e2af5be9af2a45fc3ee7cdcb68d9bcc37a3c81Saving successful migration to network...
  ... 0x15498a1f9d2ce0f867b64cdf4b22ddff56f76xlbcd3d3a92b03b7aa4d881bacSaving artifacts...

Truffle 将合约迁移到网络并保存了构件(artifacts)。 在构建目录的 Storage.json 文件中,通过检查 networks 对象来检查它是否正确。 您应该看到类似以下内容:

"networks": {  "1525343635906": {    "events": {},    "links": {},    "address": "0xd8e2af5be9af2a45fc3ee7cdcb68d9bcc37a3c81",    "transactionHash": "0xb8ec575a9f3eca4a11a3f61170231a1816f7c68940d8487e56567adcf5c0a21e"
  }
}

1525343635906 是网络的 ID,以太坊网络主网和测试网都有固定的 ID(主网是 1)。

address 是部署合约的地址。
transactionHash 是部署合约的交易 hash。

在后面会看到如何使用。

部署多合约

Truffle 迁移的真正亮点是能进行多个合约的编译、部署和跟踪(几乎所有区块链项目都是这样)。

迁移文件不仅允许我们使用单个命令部署多个合约,还允许我们调用合约的函数,如获取这些函数的返回值并将其传递给后续合约。

我们在 contracts 目录下,添加一个新合约  InfoManager.sol, 代码如下:

pragma solidity >0.4.21;import "./Storage.sol";

contract InfoManager {

    Storage private _dataStore;

    uint private _lastAdded;    constructor(Storage dataStore) public {
        _dataStore = dataStore;
    }    function addData(string key, string value) public {        require((now - 1 days) > _lastAdded);
        _dataStore.addData(key, value);
    }

}

可以看出,InfoManager 依赖 Storage 合约,不仅如此 InfoManager 的构造函数还叙需要 Storage 合约作为参数。

重新修改一下  2_deploy_contracts.js 让它可以完成 InfoManager 的部署:

var Storage = artifacts.require("./Storage.sol");var InfoManager = artifacts.require("./InfoManager.sol");module.exports = function(deployer) {    // 部署 Storage
    deployer.deploy(Storage)        // 等待、直到合约部署完成
        .then(() => Storage.deployed())        // 传递 Storage 合约地址,部署 InfoManager 合约
        .then(() => deployer.deploy(InfoManager, Storage.address));
}

部署的语法是:

...deployer.deploy(`ContractName`, [`constructor params`]) // 返回一个 promise...

因为 deploy(...) 返回一个 promise, 我们可以按自己喜欢的方式处理它,不要注意的是在部署文件里,还不支持 async 

我们还可以在部署合约后自定义调用函数。例如,迁移还可以这样:

deployer.deploy(Storage)
    .then(() => Storage.deployed())
    .then((instance) => {
        instance.addData("Hello", "world")
    }).then(() => deployer.deploy(InfoManager, Storage.address));

这样在部署 InfoManager 之前,先给 Storage 添加一条数据。

这个技巧很有用,因为有时相互依赖的合约可能需要在构造函数的之外写入。

根据网络进行部署

还可以根据所处的网络不同,进行不同的部署。这对于在开发阶段使用模拟链的数据而在主网上线是使用已部署的主网合约作为输入参数到合约中都非常有用。

通过导出函数 module.exports 扩展一个参数 network 

module.exports = function(deployer, network) {    if (network == "live") {        // do one thing
    } else if (network == "development") {        // do other thing
    }
}

选择账号进行部署

module.exports 默认函数还可以公开一个账号参数,这些账号是以太坊节点或钱包 provider “暴露” 出来的,看看下面的例子:

module.exports = function(deployer, network, accounts) {    var defaultAccount;    if (network == "live") {
        defaultAccount = accounts[0]
    } else {
        defaultAccount = accounts[1]
    }
}

链接库

你还可以通过 deployer.link(...) 连接已经存在(部署)的库:

...deployer.deploy(MyLibrary);deployer.link(MyLibrary, MyContract);deployer.deploy(MyContract);
...

结论

通过使用上面的这些技术,可以把大部分区块链部署工作自动化,并减少开发去中心化应用程序涉及的大量重复工作。

参考文章:

  1. https://www.sitepoint.com/truffle-migrations-explained/

  2. Truffle 中文文档


0
305