Skip to content

Latest commit

 

History

History
631 lines (432 loc) · 21.8 KB

File metadata and controls

631 lines (432 loc) · 21.8 KB

第15节 solidity进阶


❤️💕💕欢迎来到web3的教程,在这里,将会学习到智能合约,区块链底层原理,eth和btc学习,web3或将会颠覆世界😍~Myblog:http://nsddd.top


[TOC]

solidity进阶

我们需要引入2个新的数据类型:mapping(映射) 和 address(地址)。

Addresses (地址)

以太坊区块链由 _ account _ (账户)组成,你可以把它想象成银行账户。一个帐户的余额是 _以太_ (在以太坊区块链上使用的币种),你可以和其他帐户之间支付和接受以太币,就像你的银行帐户可以电汇资金到其他银行帐户一样。

每个帐户都有一个“地址”,你可以把它想象成银行账号。这是账户唯一的标识符,它看起来长这样:

0x90F4a169B49ED1fb79287a44Bf3aDF69A2d50727

这个是我自己的测试网络地址,或许你可以测试以下,测试币是免费的,如果你想给我以太币,那么可以转我下面地址:

0x90F4a169B49ED1fb79287a44Bf3aDF69A2d50727

我们将在后面的课程中介绍地址的细节,现在你只需要了解地址属于特定用户(或智能合约)的

Mapping(映射)

我们看到了 _ 结构体 __ 数组 _映射 是另一种在 Solidity 中存储有组织数据的方法。

映射是这样定义的:

//对于金融应用程序,将用户的余额保存在一个 uint类型的变量中:
mapping (address => uint) public accountBalance;

//或者可以用来通过userId 存储/查找的用户名
mapping (uint => string) userIdToName;

映射本质上是存储和查找数据所用的键-值对。在第一个例子中,键是一个 address,值是一个 uint,在第二个例子中,键是一个uint,值是一个 string

类似于python的字典,golang的map

msg.sender

几乎每种语言都支持全局变量不是嘛,Go语言的在函数外定义的就是全局变量,而且和局部变量不一样,全局变量定义后不使用不会报错。

JavaScript最开始使用的var

java的(或称成员变量)可分两种,一种是静态变量,另一种是实例变量

在 Solidity 中,有一些全局变量可以被所有函数调用。 其中一个就是 msg.sender,它指的是当前调用者(或智能合约)的 address

注意:在 Solidity 中,功能执行始终需要从外部调用者开始。 一个合约只会在区块链上什么也不做,除非有人调用其中的函数。所以 msg.sender总是存在的。

以下是使用 msg.sender 来更新 mapping 的例子:

mapping (address => uint) favoriteNumber;

function setMyNumber(uint _myNumber) public {
  // 更新我们的 `favoriteNumber` 映射来将 `_myNumber`存储在 `msg.sender`名下
  favoriteNumber[msg.sender] = _myNumber;
  // 存储数据至映射的方法和将数据存储在数组相似
}

function whatIsMyNumber() public view returns (uint) {
  // 拿到存储在调用者地址名下的值
  // 若调用者还没调用 setMyNumber, 则值为 `0`
  return favoriteNumber[msg.sender];
}

在这个小小的例子中,任何人都可以调用 setMyNumber 在我们的合约中存下一个 uint 并且与他们的地址相绑定。 然后,他们调用 whatIsMyNumber 就会返回他们存储的 uint

使用 msg.sender 很安全,因为它具有以太坊区块链的安全保障 —— 除非窃取与以太坊地址相关联的私钥,否则是没有办法修改其他人的数据的。

msg.sender详解

当部署合约时,msg.sender是合约的所有者,如果合约中定义了一个名为“owner”的变量,则可以为其分配值(地址)msg.sender

 address owner = msg.sender

此时,变量“owner”将始终具有最初部署合约的人的地址,意味着是合约的所有者。分析这样一行合约代码:owner.transfer(msg.value),这里如果调用了回退函数(fallback函数),那么msg.value将在owner的地址传输。

一个合约的msg.sender是当前与合约交互的地址,可以是用户也可以是另一个合约。所以,如果是一个用户和合约交互,msg.sender是该用户的地址;相反,如果是另一个合约B与该合约交互,合约B的地址将变成msg.sender

总的来说,msg.sender将会是当前与某合约交互的用户或另一个合约。

💡简单的两个案例如下:

function setOwner() {
    owner = msg.sender;
}

VS

function setOwner() {
	owner = tx.origin;
}

可以这样说,msg.senderowner可以是一个合约;而tx.originowner不可能是一个合约。在一个简单的调用链中,A->B->C->DD里面的msg.sender将会是C,而tx.origin将一直是A。

但是仔细考虑一下是否真的需要使用tx.origin。因为你可能不是合约的唯一用户。其他人可能想要使用你的合约,并希望通过其他的合约与之交互。因为在Solidity中,tx.origin它遍历整个调用栈并返回最初发送调用(或事务)的帐户的地址。在智能合约中使用此变量进行身份验证会使合约容易受到类似网络钓鱼的攻击

提醒:

跟在 JavaScript 中一样, 在 Solidity 中你也可以用 ++ 使 uint 递增。

uint number = 0;
number++;
// `number` 现在是 `1`了

实战演习

我们来修改第1课的 _createZombie 方法,将僵尸分配给函数调用者吧。

  1. 首先,在得到新的僵尸 id 后,更新 zombieToOwner 映射,在 id 下面存入 msg.sender
  2. 然后,我们为这个 msg.sender 名下的 ownerZombieCount 加 1。

💡简单的一个案例如下:

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;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        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);
    }
}

Require

怎么让每个人只能调用一次函数,这个时候就需要用到Require

require函数用于:

  • 确认有效条件,例如输入

  • 确认合约声明变量是一致的

  • 从调用到外部合约返回有效值

如果正确使用,分析工具会评估合约并分辨出引起 assert 调用错误的条件和函数。正确函数代码将会避免引起调用错误的 assert 声明;如果发生就意味着合约中存在需要修复的bug。

require使得函数在执行过程中,当不满足某些条件时抛出错误,并停止执行

function sayHiToVitalik(string _name) public returns (string) {
  // 比较 _name 是否等于 "Vitalik". 如果不成立,抛出异常并终止程序
  // (敲黑板: Solidity 并不支持原生的字符串比较, 我们只能通过比较
  // 两字符串的 keccak256 哈希值来进行判断)
  require(keccak256(_name) == keccak256("Vitalik"));
  // 如果返回 true, 运行如下语句
  return "Hi!";
}

📜 对上面的解释:

Solidity 并不支持原生的字符串比较, 我们只能通过比较

keccak256生成的是散列,具有单向

如果你这样调用函数 sayHiToVitalik(“Vitalik”) ,它会返回“Hi!”。而如果调用的时候使用了其他参数,它则会抛出错误并停止执行。

因此,在调用一个函数之前,用 require 验证前置条件是非常有必要的。

一个案例

注意,只有区块链才有address地址概念,在其他语言可能是一个字符串,但是对solidity来说,不需要双引号。

require(owner == msg.sender, "only owner can call this function"),表示的是前面如果为真,正常运行,前面为假的,就报后面的错误。

如果上面的语句通过了,就会执行下面的下划线语句,下划线放前面和放后面效果是一样的, 如果有错,交易数据回滚。

address public owner = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4;
// 我的地址 -- 设置为公有 

modifier onlyOwner() {
	require(owner == msg.sender, "only owner can call this function");
	_;
}

实战演习

在我们的僵尸游戏中,我们不希望用户通过反复调用 createRandomZombie 来給他们的军队创建无限多个僵尸 —— 这将使得游戏非常无聊。

我们使用了 require 来确保这个函数只有在每个用户第一次调用它的时候执行,用以创建初始僵尸。

  1. createRandomZombie 的前面放置 require 语句。 使得函数先检查 ownerZombieCount [msg.sender] 的值为 0 ,不然就抛出一个错误。

注意:在 Solidity 中,关键词放置的顺序并不重要

  • 虽然参数的两个位置是等效的。 但是,由于我们的答案检查器比较呆板,它只能认定其中一个为正确答案。
  • 于是在这里,我们就约定把ownerZombieCount [msg.sender]放前面吧
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;

    mapping (uint => address) public zombieToOwner;
    mapping (address => uint) ownerZombieCount;

    function _createZombie(string _name, uint _dna) private {
        uint id = zombies.push(Zombie(_name, _dna)) - 1;
        zombieToOwner[id] = msg.sender;
        ownerZombieCount[msg.sender]++;
        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 {
    //下面写代码
        require(ownerZombieCount[msg.sender] == 0);
        uint randDna = _generateRandomDna(_name);
        _createZombie(_name, randDna);
    }

}

继承(Inheritance)

有个让 Solidity 的代码易于管理的功能,就是合约 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";
  }
}

由于 BabyDoge 是从 Doge 那里 inherits (继承)过来的。 这意味着当你编译和部署了 BabyDoge,它将可以访问 catchphrase()anotherCatchphrase()和其他我们在 Doge 中定义的其他公共函数。

这可以用于逻辑继承(比如表达子类的时候,Cat 是一种 Animal)。 但也可以简单地将类似的逻辑组合到不同的合约中以组织代码。

引入(Import)

代码已经够长了,我们把它分成多个文件以便于管理。 通常情况下,当 Solidity 项目中的代码太长的时候我们就是这么做的。

在 Solidity 中,当你有多个文件并且想把一个文件导入另一个文件时,可以使用 import 语句:

import "./someothercontract.sol";

contract newContract is SomeOtherContract {

}

这样当我们在合约(contract)目录下有一个名为 someothercontract.sol 的文件( ./ 就是同一目录的意思),它就会被编译器导入。

Storage与Memory

在 Solidity 中,有两个地方可以存储变量 —— storagememory

  • storage: 储存,贮藏;(信息的)存储,存储空间;
  • memory:记忆力,记性;记忆,回忆;(计算机存储器的)存储量;

Storage 变量是指永久存储在区块链中的变量。 Memory 变量则是临时的,当外部函数对某合约调用完成时,内存型变量即被移除。 你可以把它想象成存储在你电脑的硬盘或是RAM中数据的关系。

大多数时候你都用不到这些关键字,默认情况下 Solidity 会自动处理它们。 状态变量(在函数之外声明的变量)默认为“存储”形式,并永久写入区块链;而在函数内部声明的变量是“内存”型的,它们函数调用结束后消失。

然而也有一些情况下,你需要手动声明存储类型,主要用于处理函数内的 _ 结构体 __ 数组 _ 时:

contract SandwichFactory {
  struct Sandwich {
    string name;
    string status;
  }

  Sandwich[] sandwiches;

  function eatSandwich(uint _index) public {
    // Sandwich mySandwich = sandwiches[_index];

    // ^ 看上去很直接,不过 Solidity 将会给出警告
    // 告诉你应该明确在这里定义 `storage` 或者 `memory`。

    // 所以你应该明确定义 `storage`:
    Sandwich storage mySandwich = sandwiches[_index];
    // ...这样 `mySandwich` 是指向 `sandwiches[_index]`的指针
    // 在存储里,另外...
    mySandwich.status = "Eaten!";
    // ...这将永久把 `sandwiches[_index]` 变为区块链上的存储

    // 如果你只想要一个副本,可以使用`memory`:
    Sandwich memory anotherSandwich = sandwiches[_index + 1];
    // ...这样 `anotherSandwich` 就仅仅是一个内存里的副本了
    // 另外
    anotherSandwich.status = "Eaten!";
    // ...将仅仅修改临时变量,对 `sandwiches[_index + 1]` 没有任何影响
    // 不过你可以这样做:
    sandwiches[_index + 1] = anotherSandwich;
    // ...如果你想把副本的改动保存回区块链存储
  }
}

如果你还没有完全理解究竟应该使用哪一个,也不用担心 —— 在本教程中,我们将告诉你何时使用 storage 或是 memory,并且当你不得不使用到这些关键字的时候,Solidity 编译器也发警示提醒你的。

现在,只要知道在某些场合下也需要你显式地声明 storagememory就够了!

internal 和 external

publicprivate 属性之外,Solidity 还使用了另外两个描述函数可见性的修饰词:internal(内部) 和 external(外部)。

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

externalpublic 类似,只不过这些函数只能在合约之外调用 - 它们不能被合约内的其他函数调用。稍后我们将讨论什么时候使用 externalpublic

声明函数 internalexternal 类型的语法,与声明 privatepublic类 型相同:

contract Sandwich {
  uint private sandwichesEaten = 0;

  function eat() internal {
    sandwichesEaten++;
  }
}

contract BLT is Sandwich {
  uint private baconSandwichesEaten = 0;

  function eatWithBacon() public returns (string) {
    baconSandwichesEaten++;
    // 因为eat() 是internal 的,所以我们能在这里调用
    eat();
  }
}

智能合约的交互

如果我们的合约需要和区块链上的其他的合约会话,则需先定义一个 interface (接口)。

先举一个简单的栗子。 假设在区块链上有这么一个合约:

contract LuckyNumber {
  //一个映射
  mapping(address => uint) numbers;
	
  //一个共有的函数,设置了全局变量
  function setNum(uint _num) public {
    numbers[msg.sender] = _num;
  }

  function getNum(address _myAddress) public view returns (uint) {
    return numbers[_myAddress];
  }
}

这是个很简单的合约,您可以用它存储自己的幸运号码,并将其与您的以太坊地址关联。 这样其他人就可以通过您的地址查找您的幸运号码了。

现在假设我们有一个外部合约,使用 getNum 函数可读取其中的数据。

首先,我们定义 LuckyNumber 合约的 interface

contract NumberInterface {
  function getNum(address _myAddress) public view returns (uint);
}

请注意,这个过程虽然看起来像在定义一个合约,但其实内里不同:

  1. 首先,我们只声明了要与之交互的函数 —— 在本例中为 getNum —— 在其中我们没有使用到任何其他的函数或状态变量。

  2. 其次,我们并没有使用大括号({})定义函数体,我们单单用分号(;)结束了函数声明。这使它看起来像一个合约框架。

编译器就是靠这些特征认出它是一个接口的。

在我们的 app 代码中使用这个接口,合约就知道其他合约的函数是怎样的,应该如何调用,以及可期待什么类型的返回值。

pragma solidity ^0.4.19;
import "./zombiefactory.sol";
contract KittyInterface {
  function getKitty(uint256 _id) external view returns (
    bool isGestating,
    bool isReady,
    uint256 cooldownIndex,
    uint256 nextActionAt,
    uint256 siringWithId,
    uint256 birthTime,
    uint256 matronId,
    uint256 sireId,
    uint256 generation,
    uint256 genes
  );
}

contract ZombieFeeding is ZombieFactory {

  function feedAndMultiply(uint _zombieId, uint _targetDna) public {
    require(msg.sender == zombieToOwner[_zombieId]);
    Zombie storage myZombie = zombies[_zombieId];
    _targetDna = _targetDna % dnaModulus;
    uint newDna = (myZombie.dna + _targetDna) / 2;
    _createZombie("NoName", newDna);
  }
}

使用接口

继续前面 NumberInterface 的例子,我们既然将接口定义为:

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`变量做些什么
  }
}

通过这种方式,只要将您合约的可见性设置为public(公共)或external(外部),它们就可以与以太坊区块链上的任何其他合约进行交互。

处理多返回值

getKitty 是我们所看到的第一个返回多个值的函数。我们来看看是如何处理的:

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();
}

留空就和Go语言一样,返回值也是类似的,无非就是Go语言多了一个_

if 语句

if语句的语法在 Solidity 中,与在 JavaScript 中差不多:

function eatBLT(string sandwich) public {
  // 看清楚了,当我们比较字符串的时候,需要比较他们的 keccak256 哈希码
  if (keccak256(sandwich) == keccak256("BLT")) {
    eat();
  }
}

JavaScript 实现

我们只用编译和部署 ZombieFeeding,就可以将这个合约部署到以太坊了。我们最终完成的这个合约继承自 ZombieFactory,因此它可以访问自己和父辈合约中的所有 public 方法。

我们来看一个与我们的刚部署的合约进行交互的例子, 这个例子使用了 JavaScript 和 web3.js:

var abi = /* abi generated by the compiler */
var ZombieFeedingContract = web3.eth.contract(abi)
var contractAddress = /* our contract address on Ethereum after deploying */
var ZombieFeeding = ZombieFeedingContract.at(contractAddress)

// 假设我们有我们的僵尸ID和要攻击的猫咪ID
let zombieId = 1;
let kittyId = 1;

// 要拿到猫咪的DNA,我们需要调用它的API。这些数据保存在它们的服务器上而不是区块链上。
// 如果一切都在区块链上,我们就不用担心它们的服务器挂了,或者它们修改了API,
// 或者因为不喜欢我们的僵尸游戏而封杀了我们
let apiUrl = "https://api.cryptokitties.co/kitties/" + kittyId
$.get(apiUrl, function(data) {
  let imgUrl = data.image_url
  // 一些显示图片的代码
})

// 当用户点击一只猫咪的时候:
$(".kittyImage").click(function(e) {
  // 调用我们合约的 `feedOnKitty` 函数
  ZombieFeeding.feedOnKitty(zombieId, kittyId)
})

// 侦听来自我们合约的新僵尸事件好来处理
ZombieFactory.NewZombie(function(error, result) {
  if (error) return
  // 这个函数用来显示僵尸:
  generateZombie(result.zombieId, result.name, result.dna)
})

END 链接