如何与合约进行交互

 简介

你如果以前是通过原始的 Ethereum 网络与合约进行交互, 那么你可能早就意识到编写这样的交互代码是多么的复杂. 同时, 管理每个请求对应的状态也是非常复杂. 庆幸的是, Truffle 诞生了, 为你解决这些复杂性, 让与合约的交互变得简单.

 读写数据

Ethereum 网络处理读写数据的操作是有区别的, 这个区别在编写应用时非常有用. 一般情况, 写数据称为 transaction 交易, 而读数据称为 call 调用. 交易和调用的实现方式有天壤之别, 并且有如下的特性.

 Transactions 交易

交易从根本上更改了网络上的状态. 交易可以像账户之间发送以太币一样简单, 也可以像执行一个合约方法或者部署新合约一样复杂. 从定义上来讲交易的特性就是要写/更改数据. 交易通过消耗以太币来实现, 以 gas 结算, 并且交易需要一定时间来执行. 当通过交易执行合约方法时, 无法直接获取到合约方法的返回值, 因为交易并非立即执行. 一般情况, 合约方法需要以交易的形式传递给 以太坊, 但是并不返回方法的结果, 而是返回一个交易的ID. 总结来说, 交易:

  • 消耗 gas (以太币)
  • 更改网络上的数据状态
  • 并不立即执行
  • 不会返回方法值, 而是返回交易 ID.

 Calls 调用

另一个是调用, 原理完全不同. 调用可以用于执行网络上的代码, 但是这些方法不能对数据进行永久性的更改. 调用是免费执行的, 从定义上区分就是读取数据. 当通过调用执行合约的方法时, 可以立即得到返回结果. 调用总结来说:

  • 免费 (不消耗 gas)
  • 不更改网络上数据状态
  • 立即执行
  • 直接返回需要的返回值 (万岁!)

如何选择交易还是调用, 只需要考虑是读数据的需求还是写数据的需求.

 抽象简介

合约抽象是基于JS开发以太坊合约的面包和黄油(好东西, 哈哈), 简单的说, 合约抽象是一个代码包装层令合约交互更简单. 使用这种方法, 可以不再考虑执行过程中的引擎, 齿轮等. Truffle 使用内置的合约抽象 truffle-contract 模块, 合约抽象的具体内容如下:

为了方便介绍合约抽象, 我们首先需要一个合约的例子. 我们使用大家熟悉的 MetaCoin 合约举例, 使用 truffle unbox metacoin 获取相应代码.

pragma solidity ^0.4.2;

import "./ConvertLib.sol";

// 这只是货币合约的简单实现, 并非兼容标准, 也不能直接与其他货币/token合约交互.
// 如果想要创建兼容标准的token, 参考: https://github.com/ConsenSys/Tokens. 继续!

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];
    }
}

合约除了构造方法外有三个方法(sendCoin, getBalanceInEth, 和 getBalance). 所有的方法都可以以交易/调用来执行.

我们看一下 Truffle console 提供的 JS 的 MetaCoin 对象:

// 输出已经部署好的 MetaCoin 的版本. 注意获取时需要使用 Promise, 然后可以使用 .then 方法来得到实例
MetaCoin.deployed().then(function(instance) {
  console.log(instance);
});

// 输出:
//
// Contract
// - address: "0xa9f441a487754e6b27ba044a5a8eb2eec77f6b92"
// - allEvents: ()
// - getBalance: ()
// - getBalanceInEth: ()
// - sendCoin: ()
// ...

注意: 合约抽象包含的方法与合约中的方法完全一致. 还包含了指向部署好的 MetaCoin 合约的地址.

 执行合约方法

使用合约抽象可以在以太坊网络上轻松执行合约方法.

 发起交易

在 MetaCoin 项目中, 我们有三个可用的方法. 分析它们你会发现 sendCoin 是唯一一个用于修改网络状态的方法. sendCoin 方法的目的就是从一个账号发送 MetaCoin 到另一个账号, 并且这些更改是永久的.

当我们调用 sendCoin 的时候, 我们实际上是以交易的形式使用的. 下面的例子中, 我们将从一个账号发送 10 MetaCoin 到另一个账号, 并且在网络中永久存储.

var account_one = "0x1234..."; // 地址1
var account_two = "0xabcd..."; // 地址2

var meta;
MetaCoin.deployed().then(function(instance) {
  meta = instance;
  return meta.sendCoin(account_two, 10, {from: account_one});
}).then(function(result) {
  // 执行此回调, 说明交易成功执行
  alert("Transaction 交易成功!")
}).catch(function(e) {
  // 执行此回调, 说明出现错误
})

上面的代码有几个有趣的地方:

  • 我们直接调用了合约抽象的 sendCoin 方法. 这将触发一个交易(例如写数据), 而不是调用.
  • 当交易发送成功后, 回调方法只在交易执行成功后才会触发(产生区块并且记录了该交易). 这使得开发者无需手动检查交易的状态了.
  • 我们在 sendCoin 方法第三个参数传入一个对象. 可是合约中的 sendCoin 方法并没有第三个参数. 所以这里我们使用的是一个特殊对象, 作为任何方法的最后一个参数使用, 来控制交易的具体细节. 这里我们设置了交易的发起地址 from 来保证交易来自账号 account_one.

 发起调用

继续以 MetaCoin 工程为例, getBalance 方法就是从网络读取数据的好例子. 这个方法无需对网络状态产生任何改变, 因为它只是返回对应地址 MetaCoin 的余额, 我们简单看一下:

var account_one = "0x1234..."; // 一个地址

var meta;
MetaCoin.deployed().then(function(instance) {
  meta = instance;
  return meta.getBalance.call(account_one, {from: account_one});
}).then(function(balance) {
  // 执行此回调, 说明调用成功执行. 注意: 这个方法会立即回调, 没有等待时间.
  // 我们打印一下合约方法返回的余额
  console.log(balance.toNumber());
}).catch(function(e) {
  // 执行此回调, 说明出现错误
})

有两个值得注意点地方:

  • 我们需要显示调用 .call() 方法来通知以太坊网络, 我们并不进行任何持久化操作.
  • 交易发送成功后, 我们之间得到数据, 而非交易ID. 值得注意都是, 由于以太坊可以管理非常大的数据, 我们从以太坊得到的都是 BigNumber 对象, 然后通过这个对象的方法才能转换成数字.

警告⚠️: 我们直接将获取到的内容直接转换为数字是因为我们的数字非常小, 然而, 如果在数字非常大且超出了 JS 的整型范围的时候, 那么可能会出现错误或者无法预知的异常.

 捕捉事件

合约可以触发事件, 通过捕捉这些事件可以了解合约的内部执行情况. 最简单的事件处理方法就是, 分析对应交易的结果对象内的日志:

var account_one = "0x1234..."; // 地址1
var account_two = "0xabcd..."; // 地址2

var meta;
MetaCoin.deployed().then(function(instance) {
  meta = instance;  
  return meta.sendCoin(account_two, 10, {from: account_one});
}).then(function(result) {
  // result 是一个包含如下内容的对象:
  //
  // result.tx      => 交易哈希值, 字符串
  // result.logs    => 解码后的事件内容列表
  // result.receipt => 交易收据对象, 包含交易消耗的 gas 等

  // 我们可以循环处理这些事件内容 (result.logs) 来检测是否我们触发了转账 (Transfer) 事件.
  for (var i = 0; i < result.logs.length; i++) {
    var log = result.logs[i];

    if (log.event == "Transfer") {
      // 我们找到了交易
      break;
    }
  }
}).catch(function(err) {
  // 执行此回调, 说明出现错误
});

 处理交易结果

当发起交易之后, 会得到一个 result 结果对象, 里面有丰富的交易信息. 其中如下内容最有用:

  • result.tx (字符串) - 交易的哈希值(唯一标记)
  • result.logs (数组) - 解码后的事件内容列表(实际就是日志)
  • result.receipt (对象) - 交易收据

访问 truffle-contract 中的 README 来了解详细信息.

 在网络中增加新合约

在上面的例子中, 我们都是使用的已部署合约, 我们可以通过 .new() 方法来部署新合约:

MetaCoin.new().then(function(instance) {
  // 打印新部署合约的地址
  console.log(instance.address);
}).catch(function(err) {
  // 执行此回调, 说明出现错误
});

 手动指定 address 获取合约

如果合约已经部署好了, 那么会有对应的合约地址, 可以以该地址来创建合约抽象.

var instance = MetaCoin.at("0x1234...");

 给合约转账

你可能需要能直接从一个账号向另一个转账的方法, 或者触发合约的 回调方法. 可以使用下面两个方法来实现这个功能:

方法 1: 直接通过 instance.sendTransaction() 向合约发送一条数据. 这个方法和其他合约实例方法一样, 都是 promise 的, 并且接口参数与 web3.eth.sendTransaction 相同, 只是没有回调函数而已. to 参数的值如果没有指定, 将自动填充.

instance.sendTransaction({...}).then(function(result) {
  // 和上面相同的交易结果.
});

方法 2: 如果只是发送以太币, 又更简单的方法:

instance.send(web3.toWei(1, "ether")).then(function(result) {
  // 和上面相同的交易结果
});

 进阶阅读

Truffle 提供的合约抽象包含大量的工具, 用于与合约进行交互. 查看 truffle-contract 文档, 来了解更多的内容.