以太坊 web 开发者入门

月亮🌛   |     |   3642 次阅读

更新:我已经为你在 github 上创建了一个仓库,你可以下载并使用它,它展示了这篇文章中提到的大部分内容。

我经常构建使用以太坊的 web 应用程序,我把我每天使用的神奇工具集认为是理所当然的。我们的生态系统正在快速地发展,我认为很多新接触的人会感到不知所措。以太坊是一个令人惊奇的科技,但它也是新生的,没有足够的时间去渗透到专业领域。我希望让人们知道以太坊开发实际上与现代 web 开发者的工作流程是极其兼容的--将以太坊的功能集成在任何一个 web 应用中是相对容易的,并且你可以从今天就开始。

由于我想要让自己成为一个以太坊的提倡者,以向主流开发者展示光芒为使命,我已经决定把一堆分散的知识放到一个地方(我知道,这并不去中心化)。当然你需要在每一步中查阅适当的文档,但我希望这篇文章可以或多或少地向你展示所有的一切是如何组织在一起的。
如果你已经准备好开始学习,请让我成为你的精神指导。来加入到以太坊的生态系统,帮助我们征服这个世界。

第一步:获取一个区块链

目前有很多可供选择的客户端,但是我建议不要担心 geth vs parity vs pyethapp(代表即将到来的 python 客户端)。对于那些只想要一个不完整区块链,以便他们开始构建应用的人(例如你),我建议使用 testrpc 来满足所有你开发需求。一旦你安装完成后,你可以使用 testrpc 命令来启动它。

 testrpc

恭喜你,你现在拥有了一个区块链。注意 testrpc 默认是不挖区块的,但是 -b 标记允许你指定一个区块间隔(例如,1 秒)。我有很多原因喜欢这项配置,在这里我不会深入进行探讨,但请记住它是可用的。

第二步:与区块链交流

一旦你将自己的区块链运转起来,你需要一种方式与它交流。你可能已经下载了 web3.js。如果你还没有,那么你一定是一个新手。那么,继续向前并且确保你已经安装了 web3.js,然后打开一个 config.js 文件,将下列内容放进其中:

var web3 = require('web3');
var web3_provider = 'http://localhost:8545';
var _web3 = new web3();
_web3.setProvider(new web3.providers.HttpProvider(web3_provider);
exports.web3 = _web3;

任何你想要在你的后端服务器上与区块链通信的时候,只需要这样做:

var config = require('./config.js');
config.web3.eth.X

其中的 X(例如,你想使用的任何 web3 API)可以在这里发现。

第三步:编写一些智能合约

我将在这里为你节省一些时间:你将要使用 solidity 来编写智能合约。如果你认为智能合约是可怕的,千万别这么想。对于许多应用,只要你遵循一条规则:保持你智能合约的简洁性,那么编写它实际上就很容易。

有两个原因,你想要在力所能及的范围下,尽可能地总是总是总是保持你智能合约绝对的简洁性,甚至可以说是愚蠢地简洁性
1. 每一计算/存储操作花费燃料,等同于以太币,也就相当于金钱。我们正在谈论的是调用你编写的智能合约需支付 0.05 美元与 1.50 美元之间的区别。以太坊的意义不在于代替你的数据库(至少在我看是不是),所以保持逻辑简短以及存储最小化。

  1. 更高的复杂性等同于更多的地方出现问题。这是糟糕的,尤其当你的代码对人们的钱财负责任并且不能回滚的时候。请花费一分钟的时间让最后一句话沉入你的脑海。

好的,简单的合约--得到了该指导原则,让我们继续向前。

第四步:部署这些智能合约

如果你还没有听说过 truffle,那你现在一定要去试试看。我喜欢在一个 truffle 的目录里管理我的测试智能合约。关于 truffle 的一件极好的事情是你可以轻易地将其用于你的测试框架。考虑在 package.json 文件中的这个脚本:

"scripts": {
  "test": "cd truffle && truffle deploy && truffle test 
  ./myTruffleTest.js && cd .. && npm run myOtherTests"
}

它做了以下事情:1. 部署你的智能合约,2. 运行你的 truffle 测试,3. 运行你平常的测试--所有的一切在同一个脚本中!

注意你的 truffle 测试是“特殊的”,它们在你测试范围内注入了一堆区块链的东西。有许多方式可以将该信息传递给你测试套件的其余部分。我个人使用 truffle 测试来将合约地址保留在一个配置文件中,并且在我常规的 mocha 测试中导入该配置文件。只要我有了正确的地址,我就可以在任何测试中通过 web3.js 来与我的智能合约交互。无论怎样,你都可以找到最适合你的。

回到上面提到的内容。你可以部署你的智能合约,通过进入到你的 truffle 目录,输入:

truffle deploy

注意,testrpc 必须在另外一个窗口中处于运行状态!

这会打印出你刚刚部署的智能合约的地址,随后,你会用到这个地址。正如我提到的,你可以在一个 truffle 测试程序中来保存该地址,但现在,你可以拷贝并且将它粘贴到你的 config.js 文件中:

exports.contract_addr = '0xe73e8e0a4442e140aea87a4b150ef07b82492500'

第五步:调用智能合约

现在我们有了一个智能合约,我们需要调用它。好吧,这件事情看起来不怎么样--我们将要使用纯 16 进制字符串来调用智能合约。当然,有一些可以使这件事情变得更加容易,但是当来到调用智能合约的时候,我使用老派的方法来做这件事情。并且记住,我是你的老师。

第一件需要注意的事情是一切必须是 16 进制(更多细节,请查阅附录),数字、字符串等等。第二件需要注意的事情是是以太坊中的字是 256 位的。这意味着你需要在所有的字段左侧填充 0 到 64 字符。第三件需要注意的事情是类型必须在函数定义中按照规则声明。
好吧,这正变得使人难受。让我们来看一个例子吧:

function add(uint x, uint y) public constant returns (uint) {
  return x + y;
}

让我们假设你想要 12 相加。这是你如何调用该函数的过程:

1:对你紧紧打包的,权威性的函数定义进行 keccak 256 哈希,取得到的哈希值的前 4 个字节。

说什么??好吧,我没有做到这件事情,但是你可以将你的函数声明输入到这个网站中,取结果的前 8 个字符。那么我提到的权威性是什么意思呢?在以太坊中,存在权威性的类型以及简略类型(例如,uint256uint 的权威类型)。实际上我并不知道在哪里可以找到它们全部的定义,但你可以查阅以太坊 ABI 定义还有这篇博客。

无论如何,这是我们的声明看起来的形式:

add(uint256,uint256)

它返回的 keccak256 哈希是:

771602f7f25ce61b0d4f2430f7e4789bfd9e6e4029613fda01b7f2c89fbf44ad

其前 4 个字节(8 个字符)是:

771602f7

2:填充你的参数到 256 位

这是比较容易理解的:
x = 1 是:

0000000000000000000000000000000000000000000000000000000000000001

y = 2 是:

0000000000000000000000000000000000000000000000000000000000000002

它们合在一起是:

00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002

3:将这一切打包,并且添加一个 0x 前缀
可以很好的自我解释:

0x771602f700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002

现在我们有了我们的有效载荷,我们可以使用 web3 来调用该合约:

var config = require('./config.js');
var call = '0x771602f700000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002'
var to = config.contract_addr;
var res = config.web3.eth.call({ to: to, data: call });

执行完以后,你应该得到 res 的值为 3.实际上,你将会得到一个 BigNumber 对象:

res.toString()
>'3'

也许你应该读读这篇文章,学习更多关于为什么你应该在你的应用中使用 BigNumber的知识。

你可以使用我先前提到的

稍等,我们还没有完成!我仅仅向你展示了如何调用一个合约。但是如果你想要写数据到智能合约(更新状态)又会发生什么呢?上面提到的并不会行之有效!你需要使用你的私钥对一笔交易签名,但是在那之前,你需要一些以太币。

第 6 步:建立你的账户

让我们回到 truffle。在我们的测试中,我们需要添加像下列的内容:

var keys = require(`${process.cwd()}/../test/keys.json`);
it('Should send me some ether.', function() {
  assert.notEqual(keys.me.addr, null);  
  var eth = 1*Math.pow(10, 18);
  var sendObj = {
    from: accounts[0],
    value: eth,
    to: keys.me.addr
  }

  Promise.resolve(web3.eth.sendTransaction(sendObj))
  .then(function(txHash) {
    assert.notEqual(txHash, null);
    return web3.eth.getBalance(keys.me.addr)
  })
  .then(function(balance) {
    assert.notEqual(balance.toNumber(), 0);
  })
})

重要注意事项:我们实际上正在发送 1 个以太币,等价于 10^18 wei。我们总是使用以 wei 为单位的价值来进行合约的调用以及交易的产生。

现在,我在这里将会略过一步。首先你需要获取一个以太坊账户,该账户从你生成的私钥、公钥对中获得。在后端,我喜欢使用 eth-lightwallet 来管理密钥。如果你想了解更多关于 eth-lightwallet 的细节/指令,它们都在附录中了。

为了保持事情的简洁性,我们假设在我的config.js 文件中硬编码了该变量:

exports.me = {
  addr: "0x29f2f6405e6a307baded0b0672745691358e3ee6",
  pkey: "8c2bcfce3d9c4f215fcae9b215eb7c95831da0219ebfe0bb909eb951c3134515"
}

强制性的提醒:如果在该账户中有或者将要有以太币,永远不要分享你的私钥,将它上传至 github,或者将它发布在 Medium 中。
回到测试中,你可以看到,以太币从 accounts[0] (默认有大量的以太币)中转移到 me.addr中,即配置文件中的地址。

第 7 步:与你的智能合约发生交易

现在你的账户中有了一些以太币,是时候花费它了。有 3 种方式可以花费以太币:
1. 将它发送到另外一个地址
1. 调用一个智能合约函数,更新网络的状态,这需要燃料来激励矿工处理你的更新
1. 调用一个智能合约,更新状态,但同时也接受以太币作为支付(供参考,在 solidity 中有一个 payable 修改符)-- 价值将会被发送,并且你也必须支付相应燃料。
我们接下来要做的事情属于策略 2。假设我们有接下来的函数,记录了一个用户某事物的余额:

function addUserBalance(uint balance)

public returns (bool) {

  if (!accounts[msg.sender]) { throw; }

  if (accounts[msg.sender].balance + balance < accounts[msg.sender].balance) { throw; }

  accounts[msg.sender].balance += balance;

  return true;

}

注意,第二个 if 语句,这是必要的,因为在 solidity 中,加法和减法可以导致数值的上溢与下溢--要当心!同样注意在函数范围内未声明的 msg 对象。它有全部种类的信息可以供你在函数中引用。

当我们通过发送一笔交易来调用该函数的时候,我们正在要求按照下列来更新网络的全局状态:
在这个智能合约范围内的 msg.sender 的账户余额,已经增加了 balance

我们自己没有这个权利来更新状态,所以我们需要一个矿工来为我们做这件事情。我们使用燃料向他或她支付一定的服务费用,这可以转化为以太币。

为了合适地调用该函数,我们需要使用再一次使用 ABI :

addUserBalance(uint256) --> 22526328 --> 0x225263280000000000000000000000000000000000000000000000000000000000000001

我们使用该数据组成一个未签名的交易:

var data = '0x225263280000000000000000000000000000000000000000000000000000000000000001';
var nonce = config.web3.eth.getTransactionCount(keys.me.addr);
var gasPrice = 20 * Math.pow(10, 9);
var gasLimit = 100000;var txn = {
  from: config.me.addr,
  to: config.contract_address,
  gas: `0x${gasLimit.toString(16)}`,
  gasPrice: `0x${gasPrice.toString(16)}`,
  data: data,
  nonce: `0x${nonce.toString(16)}`,
  value: '0x0'
}

正如上述提到的,燃料是产生一笔交易的必需品(例如,更新状态)。gas * gasPrice 是矿工执行你的交易过程中可以花费的 wei 的数量。如果操作花费了比你提供的更多的燃料时,该交易将不会更新状态,并且矿工将会得到所有你提供的燃料费用。如果花费了比你提供的更少的燃料,你将会被退还剩余的燃料费用。

如果我们将该对象提交至网络,它将会失败,因为没有证据表明我确实授权了该笔交易。谁知道,某个陌生人会不会更新我的余额到 10 亿(尽管不清楚为什么会有人做这件事情)。

无论如何,我所需要做的就是使用我的私钥对该交易进行签名。还记得我告诉你的不要和任何人分享你配置文件中的私钥的事情吗?使用下列代码来完成这项工作:

var Tx = require('ethereumjs-tx');
var privateKey = Buffer.from(config.me.pkey, 'hex')
var tx = new Tx(txn);
tx.sign(privateKey);
var serializedTx = tx.serialize();

这里,我们可以使用我最喜欢的来使用给定私钥对一个交易对象进行签名,这应该返回类似下列的内容:

0xf8aa808504a817c800830f424094a0f68379088f9aee95ba5c9d178693b874c4cd6880b844a9059cbb000000000000000000000000053b2188b0b100e68299708864e2ccecb62cdf0d000000000000000000000000000000000000000000000000000000746a5288001ca01f683f083c2d7c741a1218efc0144adc1749125a9ca53134b06353a8e4ef72afa07c50fb59647ff8b8895b75795b0f51de745fa5987b985f7d1025eb346755bca0

现在,最终,我们可以通过 web3 将该交易提交至区块链。它将会返回一个交易哈希,即提供的交易的哈希值(特别重要的是,这并不是该交易成功的证明!)

var txHash = config.web3.eth.sendRawTransaction(raw_txn);

看起来向下列内容:

0xac8914ecb06b333a9e655a85a0cd0cccddb8ac627098e7c40877d27a130a7293

现在,还有最后一步,严格来说是可选的,但是极其重要的,即验证你的交易已经被接受,并且被处理:获取你交易的收据。

var txReceipt = config.web3.eth.getTransactionReceipt(txHash);

如果返回 null,你的交易没有被处理(也许是你使用了错误的私钥签名?)。如果返回的不是 null,仍然会存在其它各种各样的线索提示你的交易失败,目前我不打算对其进行深入探讨。
好吧,就一个提示:如果你的 gasUsed 等于发送的 gas 总量,它意味着你的函数调用失败了。这意味着,要么 1. 你没有提供足够的燃料并且/或者2. 你的合约发生了异常。

那是一个扑朔迷离的事情!

结论

我知道这包括了很多内容。

如果你感到不知所措,我建议你放慢一点,将这篇文章作为一个参考。你或许会在每一步花费大量时间去阅读文献,才能玩转它。
也就是说,我上述所描述的内容占智能合约这场战斗的 80%。一旦你精通了这些内容,我个人会认为你是一个有能力的以太坊开发者。
如果你感兴趣,开始去做吧!这些工具正在变得越来越好,踏进智能合约的大门不能再容易了,欢迎来到智能合约的世界。
如果你喜欢这篇文章,在推特上关注我,或者加入社区。同样可以关注我的公司,ConsenSys。因为我们在做一些很棒的事情。

附录:
16 进制字符串:
我意识到我没有选择针对 16 进制数字的最好示例(由于 1 在 16 进制与 10 进制概念中是同一个值)。相反如果你使用下面的参数调用我们的 add 函数:

0000000000000000000000000000000000000000000000000000000000000100

实际上,你增加的数值是 256。所以对于任何的函数调用,最好的做法是:

var _x = 100;
var x = `${x}.toString(16)`;

这会保证你总是以 16 进制值在调用函数。

密钥简要介绍
如果你卡在如何生成密钥这一步上,你可以去看我在应用程序中使用的这个文件:

/**
 * Generate a test keypair
 */
var keystore = require('eth-lightwallet').keystore;
var Promise = require('bluebird').Promise;
var crypto = require('crypto');
var fs = Promise.promisifyAll(require('fs'));
var jsonfile = Promise.promisifyAll(require('jsonfile'));
var KEYSTORE_DIRECTORY = `${process.cwd()}/test`;
var name = process.argv[2];
var password = "test";
/**
 * Run the generator
 */
createKeystore(password)
.then(function(ks) {
  return saveProfile(name, ks.keystore, ks.privateKey, ks.address); })
.then(function(saved) { console.log('saved', saved); })
.catch(function(error) { console.log(error); });
/**
  * Utility Functions
  */
function createKeystore(_password) {
  return new Promise(function(resolve, reject) {
    var password = Buffer(_password).toString('hex');
    keystore.createVault({ password: password }, function(error, ks) {
      if (error) { reject(error); }
      ks.keyFromPassword(password, function(error, dKey) {
        if (error) { reject(error); }
        ks.generateNewAddress(dKey, 1);
        var address = `0x${ks.getAddresses()[0]}`;
        var privateKey = ks.exportPrivateKey(address, dKey);
        var keystore = JSON.parse(ks.serialize());
        resolve({ address, privateKey, keystore });
      });
    });
  });
}function saveProfile(name, keystore, privateKey, address) {
  return new Promise((resolve, reject) => {
    jsonfile.readFileAsync(`${KEYSTORE_DIRECTORY}/keys.json`, {throws: false})
    .then(function(PROFILES) {
      var profiles = PROFILES || {};
      profiles[`${name}`] = {
        keystore,
        privateKey,
        address
      };
      console.log('profiles', profiles)
      return profiles;
    })
    .then(function(_profiles) {
      return jsonfile.writeFileAsync(`${KEYSTORE_DIRECTORY}/keys.json`, _profiles, {spaces: 2});
    })
    .then(function() { resolve(true); })
    .catch(function(error) { reject(error); });
  })
}

这将会保存 keystore 到你命名的 test/keys.json 文件中。你可以使用 node keygen.js <account_name> 来运行它,这应该会起作用。我使用这个来生成测试账户,但如果你需要,例如自动生成账户,你可以复用该函数。

 
2 人喜欢