cryptoZombie 知识点总结

  • 声明版本

pragma solidity ^0.4.19;

contract HelloWorld {

}
  • 声明状态变量

contract Example {
  // 这个无符号整数将会永久的被保存在区块链中
  uint myUnsignedInteger = 100;
}

// 其中 uint 实际上指 uint256, 又分 uint8/uint16/uint32 等位数
  • 运算

加法: x + y
减法: x - y,
乘法: x * y
除法: x / y
取模 / 求余: x % y (例如, 13 % 53, 因为13除以5,余3)
乘方 x ** y = x^y
  • 结构体

struct Person {
  uint age;
  string name;
}
  • 数组

// 分为静态数组和动态数组

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

// 公共数组
Person[] public people;
// 其它的合约可以从这个数组读取数据(但不能写入数据),所以这在合约中是一个有用的保存公共数据的模式
  • 函数

function eatHamburgers(string _name, uint _amount) {

}
  • 私有 / 公共函数

// 我们合约中的其它函数才能够调用这个函数
// 继承自父合约的子合约也不可以调用这个函数
uint[] numbers;

function _addToArray(uint _number) private {
  numbers.push(_number);
}
  • 函数更多属性

// 返回值
string greeting = "What's up dog";

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

// 修饰符 view 只能读取数据不能更改数据
function sayHello() public view returns (string) {}

// 修饰符 pure 这个函数甚至都不访问应用里的数据
function _multiply(uint a, uint b) private pure returns (uint) {
  return a * b;
}
  • Keccak256 和 类型转换

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

uint8 a = 5;
uint b = 6;
// 将会抛出错误,因为 a * b 返回 uint, 而不是 uint8:
uint8 c = a * b;
// 我们需要将 b 转换为 uint8:
uint8 c = a * uint8(b);
  • 事件

// 这里建立事件
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;
}

// 前端监听
YourContract.IntegersAdded(function(error, result) {
  // 干些事
})
  • 使用 web3.js

// 下面是调用合约的方式:
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
}
  • 映射(Mapping)和地址(Address)

// 以太坊区块链由 _ account _ (账户)组成

//  _映射_ 是另一种在 Solidity 中存储有组织数据的方法
映射可以理解为一个包含一个或多个 key-value 键值对的对象
  • Msg.sender

全局变量,指的是当前调用者(或智能合约)的 address
  • Require

// require 使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行
function sayHiToVitalik(string _name) public returns (string) {
  // 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
  // (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
  // 两字符串的 keccak256 哈希值来进行判断)
  require(keccak256(_name) == keccak256("Vitalik"));
  // 如果返回 true, 运行如下语句
  return "Hi!";
}
  • 继承(Inheritance)

contract Doge {
  function catchphrase() public returns (string) {
    return "So Wow CryptoDoge";
  }
}

contract BabyDoge is Doge {
  function anotherCatchphrase() public returns (string) {
    return "Such Moon BabyDoge";
  }
}
  • 引入(Import)

import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}
  • Storage与Memory

// Storage 变量是指永久存储在区块链中的变量。 
// Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。
  • internal(内部) 和 external(外部)

// internal 和 private 类似,不过, 如果某个合约继承自其父合约,这个合约即可以访问父合约中定义的“内部”函数。

// external 与public 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。
  • 与其他合约的交互-接口

// 先定义一个 interface
// 不使用大括号({ 和 })定义函数体,我们单单用分号(;)结束了函数声明
contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}

// 在自己的合约中使用
contract MyContract {
  address NumberInterfaceAddress = 0xab38...;
  // ^ 这是FavoriteNumber合约在以太坊上的地址
  NumberInterface numberContract = NumberInterface(NumberInterfaceAddress);
  // 现在变量 `numberContract` 指向另一个合约对象

  function someFunction() public {
    // 现在我们可以调用在那个合约中声明的 `getNum`函数:
    uint num = numberContract.getNum(msg.sender);
    // ...在这儿使用 `num`变量做些什么
  }
}
  • 处理多返回值

function multipleReturns() internal returns(uint a, uint b, uint c) {
  return (1, 2, 3);
}

function processMultipleReturns() external {
  uint a;
  uint b;
  uint c;
  // 这样来做批量赋值:
  (a, b, c) = multipleReturns();
}

// 或者如果我们只想返回其中一个变量:
function getLastReturnValue() external {
  uint c;
  // 可以对其他字段留空:
  (,,c) = multipleReturns();
}
  • Ownable Contracts

// OpenZeppelin库的Ownable 合约
contract Ownable {
  address public owner;
  event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);

  // function Ownable()是一个 _ constructor_ (构造函数),构造函数不是必须的,它与合约同名,构造函数一生中唯一的一次执行,就是在合约最初被创建的时候。
  function Ownable() public {
    owner = msg.sender;
  }

  // 在其他语句执行前,为它检查下先验条件。 在这个例子中,我们就可以写个修饰符 onlyOwner 检查下调用者,确保只有合约的主人才能运行本函数
  modifier onlyOwner() {
    require(msg.sender == owner);
    _;
  }

  /**
   * @dev Allows the current owner to transfer control of the contract to a newOwner.
   * @param newOwner The address to transfer ownership to.
   */
  function transferOwnership(address newOwner) public onlyOwner {
    require(newOwner != address(0));
    OwnershipTransferred(owner, newOwner);
    owner = newOwner;
  }
}
  • 合约继承 Ownable

import "./ownable.sol";
contract ZombieFactory is Ownable {}
  • gas优化1

// 把 uint 绑定到 struct 里面
// 如果一个 struct 中有多个 uint,则尽可能使用较小的 uint, Solidity 会将这些 uint 打包在一起,从而占用较少的存储空间
struct NormalStruct {
  uint a;
  uint b;
  uint c;
}

struct MiniMe {
  uint32 a;
  uint32 b;
  uint c;
}

// 因为使用了结构打包,`mini` 比 `normal` 占用的空间更少
NormalStruct normal = NormalStruct(10, 20, 30);
MiniMe mini = MiniMe(10, 20, 30); 

// 当 uint 定义在一个 struct 中的时候,尽量使用最小的整数子类型以节约空间。 并且把同样类型的变量放一起(即在 struct 中将把变量按照类型依次放置)
  • 时间单位

// Solidity 使用自己的本地时间单位
// 变量 now 将返回当前的unix时间戳(自1970年1月1日以来经过的秒数)
// Solidity 还包含秒(seconds),分钟(minutes),小时(hours),天(days),周(weeks) 和 年(years) 等时间单位

// 必须使用 uint32(...) 进行强制类型转换,因为 now 返回类型 uint256。
// 所以我们需要明确将它转换成一个 uint32 类型的变量
  • gas优化2

// 使用 view 查询数据,但是注意:
// 如果一个 view 函数在另一个函数的内部被调用,
// 而调用函数与 view 函数的不属于同一个合约,也会产生调用成本。
//这是因为如果主调函数在以太坊创建了一个事务,它仍然需要逐个节点去验证。
//所以标记为 view 的函数只有在外部调用时才是免费的。(在所能只读的函数上标记上表示“只读”的“external view 声明)
  • 修饰符 payable

一种可以接收以太的特殊函数

contract OnlineStore {
  function buySomething() external payable {
    // 检查以确定0.001以太发送出去来运行函数:
    require(msg.value == 0.001 ether);
    // 如果为真,一些用来向函数调用者发送数字内容的逻辑
    transferThing(msg.sender);
  }
}
  • 随机数

这个方法首先拿到now的时间戳、msg.sender、 以及一个自增数nonce(一个仅会被使用一次的数,这样我们就不会对相同的输入值调用一次以上哈希函数了)。

// 生成一个0到100的随机数:
uint randNonce = 0;
uint random = uint(keccak256(now, msg.sender, randNonce)) % 100;
randNonce++;
uint random2 = uint(keccak256(now, msg.sender, randNonce)) % 100;
  • 转移标准

ERC721 规范有两种不同的方法来转移代币

  1. 第一种方法是代币的拥有者调用transfer 方法,传入他想转移到的 address 和他想转移的代币的 _tokenId。

  2. 第二种方法是代币拥有者首先调用 approve,然后传入与以上相同的参数。接着,该合约会存储谁被允许提取代币,通常存储到一个 mapping (uint256 => address) 里。然后,当有人调用 takeOwnership 时,合约会检查 msg.sender 是否得到拥有者的批准来提取代币,如果是,则将代币转移给他。

  • 预防溢出 假设我们有一个uint8, 只能存储8 bit数据。这意味着我们能存储的最大数字就是二进制11111111(或者说十进制的 2^8 - 1 = 255)

uint8 number = 255;
number++;
// number = 0
  • 使用safeMath

import "safemath.sol"

contract Hello{
  // 声明了 using SafeMath for uint 后,我们用来调用这些方法的 uint 就自动被作为第一个参数传递进去了
  using SafeMath for uint;
  using SafeMath32 for uint32;
  using SafeMath16 for uint16;
  uint test = 2;
  test = test.mul(3); // 等于 test = test * 3
}
  • 注释 - 使用 natspec 标准

/// @title 一个简单的基础运算合约
/// @author H4XF13LD MORRIS 💯💯😎💯💯
/// @notice 现在,这个合约只添加一个乘法
contract Math {
  /// @notice 两个数相乘
  /// @param x 第一个 uint
  /// @param y  第二个 uint
  /// @return z  (x * y) 的结果
  /// @dev 现在这个方法不检查溢出
  function multiply(uint x, uint y) returns (uint z) {
    // 这只是个普通的注释,不会被 natspec 解释
    z = x * y;
  }
}