CryptoZombies以太坊学习 - 01

僵尸工场

第一步 设置一个空合约

1
2
3
4
5
6
pragma solidity ^0.4.19;

// 创建一个僵尸工场的空合约
contract ZombieFactory {

}

第二步 状态变量和整数

状态变量是被永久地保存在合约中,也就是说他们被写入以太币区块链中,想象成写入一个数据库

1
2
3
4
5
6
7
8

contract ZombieFactory {
//uint 无符号数据类型,指其值不能是附属,对于有符号的整数存在名为int 的数据类型
// Solidity 中uint是uint256的代名词,一个256位的无符号整数 还有uint8 uint16 uint 32一般用uint就可以了

//定义僵尸的DNA由一个十六位数字组成
uint dnaDigits = 16;
}

第三步 数学运算

加法 x + y
减法 x - y
乘法 x * y
除法 x / y
求模/取余 x % y
乘方 x ** y

1
2
3
4
5
6
7
8
contract ZombieFactory {

uint dnaDigits = 16;

// 为了保证我们的僵尸的DNA只含有16个字符,我们先造一个uint数据,让它等于10^16。这样一来以后我们可以用模运算符 % 把一个整数变成16位
// 建立一个uint类型的变量,名字叫dnaModulus 令其等于10的dnaDigits次方
uint dnaModulus = 10 ** dnaDigits;
}

第四步 结构体

在我们的程序中,我们将创建一些僵尸!每个僵尸将拥有多个属性,所以这是一个展示结构体的完美例子。

建立一个struct 命名为 Zombie.

我们的 Zombie 结构体有两个属性: name (类型为 string), 和 dna (类型为 uint)。

1
2
3
4
5
6
7
8
9
10
11
contract ZombieFactory {

uint dnaDigits = 16;

uint dnaModulus = 10 ** dnaDigits;
// 定义一个结构体
struct Zombie {
string name;
uint dna;
}
}

第五步 数组

如果你想建立一个集合,可以用 数组这样的数据类型. Solidity 支持两种数组: 静态 数组和动态 数组:

固定长度为2的静态数组:
1
2
3
4
5
uint[2] fixedArray;
// 固定长度为5的string类型的静态数组:
string[5] stringArray;
// 动态数组,长度不固定,可以动态添加元素:
uint[] dynamicArray;

你也可以建立一个 结构体类型的数组 例如,上一章提到的 Person:
Person[] people; // dynamic Array, we can keep adding to it

记住:状态变量被永久保存在区块链中。所以在你的合约中创建动态数组来保存成结构的数据是非常有意义的。

公共数组

你可以定义 public 数组, Solidity 会自动创建 getter 方法. 语法如下:

Person[] public people;

其它的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
contract ZombieFactory {

uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;

struct Zombie {
string name;
uint dna;
}

// 创建一个数据类型为 Zombie 的结构体数组,用 public 修饰,命名为:zombies.
Zombie[] public zombies;
}

第六步 定义函数

在 Solidity 中函数定义的句法如下:

function eatHamburgers(string _name, uint _amount) {}
这是一个名为 eatHamburgers 的函数,它接受两个参数:一个 string类型的 和 一个 uint类型的。现在函数内部还是空的。

注:: 习惯上函数里的变量都是以(_)开头 (但不是硬性规定) 以区别全局变量。我们整个教程都会沿用这个习惯。

我们的函数调用如下:

eatHamburgers("vitalik", 100);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;

//建立一个函数 createZombie。 它有两个参数: _name (类型为string), 和 _dna (类型为uint)。
function createZombie(string _name, uint _dna) {

}

}

第七步

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;

function createZombie(string _name, uint _dna) {
//在函数体里新创建一个 Zombie, 然后把它加入 zombies 数组中。 新创建的僵尸的 name 和 dna,来自于函数的参数。
zombies.push(Zombie(_name, _dna));
}

}
```

### 第八步 变为私有函数
```solidity
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
// 变为私有private
function _createZombie(string _name, uint _dna) private {
//在函数体里新创建一个 Zombie, 然后把它加入 zombies 数组中。 新创建的僵尸的 name 和 dna,来自于函数的参数。
zombies.push(Zombie(_name, _dna));
}

}
```

### 第九步 函数返回值和修饰符

#### 返回值
要想函数返回一个数值,按如下定义:

string greeting = “What’s up dog”;

function sayHello() public returns (string) {
return greeting;
}

1
2
3
4
5
6
7
8
9
10
Solidity 里,函数的定义里可包含返回值的数据类型(如本例中 string)。

#### 函数的修饰符
上面的函数实际上没有改变 Solidity 里的状态,即,它没有改变任何值或者写任何东西。

这种情况下我们可以把函数定义为 view, 意味着它只能读取数据不能更改数据:

`function sayHello() public view returns (string) {}`

Solidity 还支持 pure 函数, 表明这个函数甚至都不访问应用里的数据,例如:

function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
这个函数甚至都不读取应用里的状态 — 它的返回值完全取决于它的输入参数,在这种情况下我们把函数定义为 pure.

注:可能很难记住何时把函数标记为 pure/view。 幸运的是, Solidity 编辑器会给出提示,提醒你使用这些修饰符。

```solidity
pragma solidity ^0.4.19;

contract ZombieFactory {

uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;

struct Zombie {
string name;
uint dna;
}

Zombie[] public zombies;

function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
// 创建一个产生随机数字的函数,不改变数据,只访问数据,使用view
function _generateRandomDna(string _str) private view returns (uint) {

}

}

第十步 Keccak256 和 类型转换

keccak256

如何让 _generateRandomDna 函数返回一个全(半) 随机的 uint?

Ethereum 内部有一个散列函数keccak256,它用了SHA3版本。一个散列函数基本上就是把一个字符串转换为一个256位的16进制数字。字符串的一个微小变化会引起散列数据极大变化。

这在 Ethereum 中有很多应用,但是现在我们只是用它造一个伪随机数。

例子:

keccak256("aaaab");//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5

keccak256("aaaac");//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
显而易见,输入字符串只改变了一个字母,输出就已经天壤之别了。

注: 在区块链中安全地产生一个随机数是一个很难的问题, 本例的方法不安全,但是在我们的Zombie DNA算法里不是那么重要,已经很好地满足我们的需要了。

类型转换

有时你需要变换数据类型。例如:

1
2
3
4
5
6
7
uint8 a = 5;
uint b = 6;
// 将会抛出错误,因为 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我们需要将 b 转换为 uint8:
uint8 c = a * uint8(b);
上面, a * b 返回类型是 uint, 但是当我们尝试用 uint8 类型接收时, 就会造成潜在的错误。如果把它的数据类型转换为 uint8, 就可以了,编译器也不会出错。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
pragma solidity ^0.4.19;

contract ZombieFactory {

uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;

struct Zombie {
string name;
uint dna;
}

Zombie[] public zombies;

function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
// 第一行代码取 _str 的 keccak256 散列值生成一个伪随机十六进制数,类型转换为 uint, 最后保存在类型为 uint 名为 rand 的变量中。

// 我们只想让我们的DNA的长度为16位 (还记得 dnaModulus?)。所以第二行代码应该 return 上面计算的数值对 dnaModulus 求余数(%)
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModuls;
}

}

第十一步 放在一起

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
pragma solidity ^0.4.19;

contract ZombieFactory {

uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;

struct Zombie {
string name;
uint dna;
}

Zombie[] public zombies;

function _createZombie(string _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}

function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}

//创建一个 public 函数,命名为createRandomZombie. 它将被传入一个变量 _name (数据类型是 string)。 (注: 定义公共函数 public 和定义一个私有 private 函数的做法一样)。

//函数的第一行应该调用 _generateRandomDna 函数,传入 _name 参数, 结果保存在一个类型为 uint 的变量里,命名为 randDna。

//第二行调用 _createZombie 函数, 传入参数: _name 和 randDna。

//整个函数应该是4行代码 (包括函数的结束符号 } )。
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}

}

第十二步 事件

我们的合约几乎就要完成了!让我们加上一个事件.

事件 是合约和区块链通讯的一种机制。你的前端应用“监听”某些事件,并做出反应。

例子:

1
2
3
4
5
6
7
8
9
// 这里建立事件
event IntegersAdded(uint x, uint y, uint result);

function add(uint _x, uint _y) public {
uint result = _x + _y;
//触发事件,通知app
IntegersAdded(_x, _y, result);
return result;
}

你的 app 前端可以监听这个事件。JavaScript 实现如下:

1
2
3
YourContract.IntegersAdded(function(error, result) { 
// 干些事
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
pragma solidity ^0.4.19;

contract ZombieFactory {

// 这里建立事件
event NewZombie(uint zombieId, string name, uint dna);
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;

struct Zombie {
string name;
uint dna;
}

Zombie[] public zombies;

function _createZombie(string _name, uint _dna) private {
// 这里触发事件,修改 _createZombie 函数使得当新僵尸造出来并加入zombies数组后,生成事件NewZombie。
//需要定义僵尸id。 array.push() 返回数组的长度类型是uint - 因为数组的第一个元素的索引是 0, array.push() - 1 将是我们加入的僵尸的索引。 zombies.push() - 1 就是 id,数据类型是 uint。在下一行中你可以把它用到 NewZombie 事件中。
uint id = zombies.push(Zombie(_name, _dna)) - 1;
NewZombie(id, _name, _dna);
}


function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(_str));
return rand % dnaModulus;
}

function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}

第十三步 Web3.js

我们的 Solidity 合约完工了! 现在我们要写一段 JavaScript 前端代码来调用这个合约。

以太坊有一个 JavaScript 库,名为Web3.js。

在后面的课程里,我们会进一步地教你如何安装一个合约,如何设置Web3.js。 但是现在我们通过一段代码来了解 Web3.js 是如何和我们发布的合约交互的吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 下面是调用合约的方式:
var abi = /* abi是由编译器生成的 */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* 发布之后在以太坊上生成的合约地址 */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// `ZombieFactory` 能访问公共的函数以及事件

// 某个监听文本输入的监听器:
$("#ourButton").click(function(e) {
var name = $("#nameInput").val()
//调用合约的 `createRandomZombie` 函数:
ZombieFactory.createRandomZombie(name)
})

// 监听 `NewZombie` 事件, 并且更新UI
var event = ZombieFactory.NewZombie(function(error, result) {
if (error) return
generateZombie(result.zombieId, result.name, result.dna)
})

// 获取 Zombie 的 dna, 更新图像
function generateZombie(id, name, dna) {
let dnaStr = String(dna)
// 如果dna少于16位,在它前面用0补上
while (dnaStr.length < 16)
dnaStr = "0" + dnaStr

let zombieDetails = {
// 前两位数构成头部.我们可能有7种头部, 所以 % 7
// 得到的数在0-6,再加上1,数的范围变成1-7
// 通过这样计算:
headChoice: dnaStr.substring(0, 2) % 7 + 1,
// 我们得到的图片名称从head1.png 到 head7.png

// 接下来的两位数构成眼睛, 眼睛变化就对11取模:
eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
// 再接下来的两位数构成衣服,衣服变化就对6取模:
shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
//最后6位控制颜色. 用css选择器: hue-rotate来更新
// 360度:
skinColorChoice: parseInt(dnaStr.substring(6, 8) / 100 * 360),
eyeColorChoice: parseInt(dnaStr.substring(8, 10) / 100 * 360),
clothesColorChoice: parseInt(dnaStr.substring(10, 12) / 100 * 360),
zombieName: name,
zombieDescription: "A Level 1 CryptoZombie",
}
return zombieDetails
}