目录
学习目的
做一个关于zombie的dapp,学习solidity及其相关内容。
每次写合约的基本骨架
以ZombieFactory命名
pragma solidity >=0.5.0 <0.6.0;//solidity version
contract ZombieFactory{
}
状态变量 State variables
State variables are permanently stored in contract storage. This means they're written to the Ethereum blockchain. Think of them like writing to a DB.
状态变量永久存储在合约存储中,这意味着会写入以太坊区块链中。
也可以认为它们写入了数据库中。
无符号整型: uint
uint非负。
注意:在Solidity中,uint实际上是uint256(一个256位无符号整数)的别名。您可以用更少的位来声明uint-uint8、uint16、uint32等。但一般情况下只需要使用uint。
声明一个名为dnaDigits的uint,并将其设置为16。
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16; //start here
}
数学运算 Math Operation
+ , - , * , / ,% 基本运算与习惯一致,不过Solidity还支持指数运算符 **
uint x = 5 ** 2; // equal to 5^2 = 25
为了确保僵尸的DNA只有16个字符,让我们制作另一个等于10^16的uint。这样,我们可以稍后使用模运算符%将整数缩短为16位。
创建一个名为dnaModules的uint,并将其设置为dnaDigits的10次方。
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
//start here
uint dnaModulus = 10 ** dnaDigits;
}
结构体 Structs
类似C语言的结构体
struct Person {
uint age;
string name;
}
注意,我们刚刚引入了一个新类型string。字符串用于任意长度的UTF-8数据。
dapp中,我们将要创建一些僵尸。僵尸将具有多个属性,因此这是结构的完美用例。
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
// start here
struct Zombie {
uint dna;
string name;
}
}
数组 Arrays
Solidity中有两种类型的数组:固定fixed 数组和动态dynamic 数组
还可以创建结构数组。使用的Zombie 结构:
Zombie[] zombie;
还记得状态变量永久存储在区块链中吗?因此,像这样创建一个动态结构数组对于在合同中存储结构化数据(有点像数据库)非常有用。
公共数组
您可以将数组声明为public,Solidity将自动为其创建getter方法。语法如下:
Zombie[] public Zombie;
这样别的合约可以读它,但不能写入它。
我们想在我们的应用程序中存储一支僵尸大军。我们将向其他应用程序展示我们所有的僵尸,所以我们希望它公开。
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
// start here
Zombie[] public zombies;
}
函数声明 Function Declarations
类似C语言。
有两种方法可以将参数传递给Solidity函数:
按值。这意味着Solidity编译器将创建参数值的新副本并将其传递给函数。这允许您的函数修改值,而不必担心初始参数的值发生更改。
通过引用。这意味着使用对原始变量的…引用来调用函数。因此,如果函数更改了它接收的变量的值,则原始变量的值将更改。
注意:为了将函数参数变量名称与全局变量区分开来,通常(但不是必须)以下划线(_)开头。我们将在整个学习过程中使用该约定。
示例 :
我们提供了有关_name变量应存储在内存中的位置的说明。这是所有引用类型(如数组、结构、映射和字符串)所必需的。
function eatHamburgers(string memory _name, uint _amount) public {
}
创建名为createZombie的公共函数。它应该包含两个参数:_name(字符串)和_dna(uint)。不要忘记使用memory关键字按值传递第一个参数
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
// start here
function createZombie(string memory _name, uint _dna) public {
}
}
结构和数组的使用
使用array.push()把对象压入对象数组。
struct Person {
uint age;
string name;
}
Person[] public people;
// create a New Person:
Person satoshi = Person(172, "Satoshi");
// Add that person to the Array:
people.push(satoshi); //或者 people.push(Person(16, "Vitalik"));
完善函数体,使其创建一个新的僵尸,并将其添加到僵尸数组中。新僵尸的名字和dna应该来自函数参数。
让我们在一行代码中完成它,以保持整洁。
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function createZombie (string memory _name, uint _dna) public {
// start here
zombies.push(Zombie(_name, _dna));
}
}
私有/公有的函数
在Solidity中,默认情况下函数是public的。
修改为私有如下示例:
uint[] numbers;
function _addToArray(uint _number) private {
numbers.push(_number);
}
如你所见, 在函数名后修改为private即可。惯例是以下划线()开头的私有函数名。
修改createZombie,使其成为私有函数。别忘了命名惯例!
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
}
More on Functions
返回值
string greeting = "What's up dog";
function sayHello() public returns (string memory) {
return greeting;
}
在Solidity中,函数声明包含返回值returns的类型(在本例中为字符串)。
函数修饰符
我们可以将其声明为一个视图view函数,这意味着它只查看数据,而不修改数据:
function sayHello() public view returns (string memory) {
}
Solidity还包含纯pure函数,这意味着无法访问应用程序中的任何数据:
function _multiply(uint a, uint b) private pure returns (uint) {
return a * b;
}
这个函数甚至不会从app的状态中读取数据——它的返回值只取决于它的函数参数。所以在本例中,我们将函数声明为纯函数。
注意:可能很难记住何时将函数标记为pure/view。幸运的是,Solidity编译器会发出警告,让您知道何时应该使用这些修饰符之一。
运用
创建名为_generateRandomDna的专用函数。它将接受一个名为_str(a string)的参数,并返回一个uint。不要忘记将_str参数的数据位置设置为memory。
此函数将查看合约的一些变量,但不会修改它们,因此将其标记为view。
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
function _generateRandomDna(string memory _str) private view returns (uint) {
}
}
Keccak256 and Typecasting
Keccak256
我们希望_generateRandomDna函数返回一个(半)随机uint。我们如何才能做到这一点?
以太坊内置了哈希函数keccak256,这是SHA3的一个版本。哈希函数基本上将输入映射为随机的256位十六进制数。输入中的轻微变化将导致哈希值的大幅变化。
它在以太坊中有很多用途,但目前我们只打算将其用于伪随机数生成。
同样重要的是,keccak256需要一个字节类型的参数。这意味着我们必须在调用keccak256之前“打包”任何参数:
//6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5
keccak256(abi.encodePacked("aaaab"));
//b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9
keccak256(abi.encodePacked("aaaac"));
上述代码可以发现,输入中只有1个字符的更改,但返回的值完全不同。
注意:在区块链中安全生成随机数是一个非常困难的问题。我们这里的方法是不安全的,但由于安全并不是僵尸DNA的首要任务,所以它对于我们的目的来说已经足够好了。
Typecasting
数据类型之间的转换。示例:
uint8 a = 5;
uint b = 6;
// throws an error because a * b returns a uint, not uint8:
uint8 c = a * b;
// we have to typecast b as a uint8 to make it work:
uint8 c = a * uint8(b);
运用
第一行代码应该采用abi.encodePacked(_str)的keccak256哈希生成十六进制伪随机数,将其类型化为uint,最后将结果存储在名为rand的uint中。
我们希望DNA只有16位长(还记得我们的dnaModulus吗?)。因此,第二行代码应返回上述值modulus (%)dnaModulus。
pragma solidity ^0.4.25;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
}
整合一下
我们的随机僵尸生成器即将完成!让我们创建一个将一切联系在一起的公共函数。
我们将创建一个公共函数,该函数接受输入僵尸的名称,并使用该名称创建具有随机DNA的僵尸。
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
uint dnaDigits = 16;
uint dnaModulus = 10 ** dnaDigits;
struct Zombie {
string name;
uint dna;
}
Zombie[] public zombies;
function _createZombie(string memory _name, uint _dna) private {
zombies.push(Zombie(_name, _dna));
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;//保证16位
}
// start here
function createRandomZombie(string memory _name) public{
uint randDna = _generateRandomDna(_name);
_createZombie(_name,randDna);
}
}
事件 Events
我们的合约快完成了!现在,让我们添加一个事件。
事件是您的合约将区块链上发生的事情通知给您的app前端的一种方式,它可以“监听”某些事件,并在事件发生时动作。
示例:
// 声明事件
event IntegersAdded(uint x, uint y, uint result);
function add(uint _x, uint _y) public returns (uint) {
uint result = _x + _y;
// emit触发一个事件让前端知道该函数动作了:
emit IntegersAdded(_x, _y, result);
return result;
}
然后,你的app前端可以监听事件。javascript实现类似于:
YourContract.IntegersAdded(function(error, result) {
// 写带结果的
})
运用
我们希望每次创建新僵尸时,都有一个事件让前端知道,这样应用程序就可以显示它。
Declare an event called NewZombie. It should pass zombieId (a uint), name (a string), and dna (a uint).
Modify the _createZombie function to fire the NewZombie event after adding the new Zombie to our zombies array.
You're going to need the zombie's id. array.push() returns a uint of the new length of the array - and since the first item in an array has index 0, array.push() - 1 will be the index of the zombie we just added. Store the result of zombies.push() - 1 in a uint called id, so you can use this in the NewZombie event in the next line.
pragma solidity >=0.5.0 <0.6.0;
contract ZombieFactory {
// declare our event here
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 memory _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
// and fire it here
emit NewZombie(id,_name,_dna);
}
function _generateRandomDna(string memory _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string memory _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
注:事件中参数不是私有的,因此不用_前缀
Web3.js
Solidity合约已完成!现在需要编写一个与合约交互的javascript前端
以太坊有一个名为Web3.js的Javascript库
后面我们将深入讨论如何部署合约和设置Web3.js。但现在,我们先看看我们部署的合约与Web3.js交互的一些示例。
// 以下是如何访问合约:
var abi = /* 编译器生成的abi */
var ZombieFactoryContract = web3.eth.contract(abi)
var contractAddress = /* 合约部署后在以太坊上的地址 */
var ZombieFactory = ZombieFactoryContract.at(contractAddress)
// `ZombieFactory` 有权访问合约的公共functions和event
// 接收文本输入的事件监听器:
$("#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)
})
// 获取僵尸dna,更新我们的图像
function generateZombie(id, name, dna) {
let dnaStr = String(dna)
// 如果DNA少于16个字符,用前补零填充
while (dnaStr.length < 16)
dnaStr = "0" + dnaStr
let zombieDetails = {
// 头两个数字组成head。我们有7个可能的heads,所以%7
// 得到一个0-6的数字,然后加上1使其为1-7。然后我们得到7
// 我们加载名为“head1.png”到“head7.png”的基于下面数字的图像文件
// 这些数字:
headChoice: dnaStr.substring(0, 2) % 7 + 1,
// 第二组两位数字组成眼睛,11种变化:
eyeChoice: dnaStr.substring(2, 4) % 11 + 1,
// 6种衬衫:
shirtChoice: dnaStr.substring(4, 6) % 6 + 1,
// 最后6位数字控制颜色。使用CSS过滤器更新:hue-rotate(angle)
// 参数angle最大为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
}
我们的javascript所做的是获取上面zombieDetails中生成的值,并使用一些基于浏览器的javascript(使用的是Vue.js)来改变图像以及应用CSS过滤器。