以太坊實戰
本篇部落格中記錄以太坊的程式碼規範和框架,部落格中的程式碼是同一個程式,逐漸增加內容的,這個程式逐漸豐富的過程
As you can see, we use the keyword private
after the function name. And as with function parameters, it's convention to start private function names with an underscore (_
).
私有函式在引數列表後加 private即可
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 _name, uint _dna) private { zombies.push(Zombie(_name, _dna)); } }
Ethereum has the hash function keccak256
built in, which is a version of SHA3. A hash function basically maps an input into a random 256-bit hexidecimal number. A slight change in the input will cause a large change in the hash.
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 _name, uint _dna) private { zombies.push(Zombie(_name, _dna)); } function _generateRandomDna(string _str) private view returns (uint) { // start here uint rand=uint(keccak256(abi.encodePacked(_str))); return rand%dnaModulus; } }
-
Create a
public
function namedcreateRandomZombie
. It will take one parameter named_name
(astring
). (Note: Declare this functionpublic
just as you declared previous functionsprivate
) -
The first line of the function should run the
_generateRandomDna
function on_name
, and store it in auint
randDna
. -
The second line should run the
_createZombie
function and pass it_name
andrandDna
. -
The solution should be 4 lines of code (including the closing
}
of the function).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 _name, uint _dna) private { zombies.push(Zombie(_name, _dna)); } function _generateRandomDna(string _str) private view returns (uint) { uint rand = uint(keccak256(abi.encodePacked(_str))); return rand % dnaModulus; } // start here function createRandomZombie(string _name) public{ uint randDna=_generateRandomDna(_name); _createZombie(_name,randDna); } }
We want an event to let our front-end know every time a new zombie was created, so the app can display it.
-
Declare an
event
calledNewZombie
. It should passzombieId
(auint
),name
(astring
), anddna
(auint
). -
Modify the
_createZombie
function to fire theNewZombie
event after adding the new Zombie to ourzombies
array. -
You're going to need the zombie's
id
.array.push()
returns auint
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 ofzombies.push() - 1
in auint
calledid
, so you can use this in theNewZombie
event in the next line
pragma solidity ^0.4.25;
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 _name, uint _dna) private {
// and fire it here
uint id=zombies.push(Zombie(_name, _dna))-1;
emit NewZombie(id,_name,_dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
最後結果如下:
To store zombie ownership, we're going to use two mappings: one that keeps track of the address that owns a zombie, and another that keeps track of how many zombies an owner has.
-
Create a mapping called
zombieToOwner
. The key will be auint
(we'll store and look up the zombie based on its id) and the value anaddress
. Let's make this mappingpublic
. -
Create a mapping called
ownerZombieCount
, where the key is anaddress
and the value auint
.
pragma solidity ^0.4.25;
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;
// declare mappings here
mapping (uint => address) public zombieToOwner;
mapping (address => uint) ownerZombieCount;
function _createZombie(string _name, uint _dna) private {
uint id = zombies.push(Zombie(_name, _dna)) - 1;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
Let's update our _createZombie
method from lesson 1 to assign ownership of the zombie to whoever called the function.
-
First, after we get back the new zombie's
id
, let's update ourzombieToOwner
mapping to storemsg.sender
under thatid
. -
Second, let's increase
ownerZombieCount
for thismsg.sender
.
In Solidity, you can increase a uint
with ++
, just like in javascript:
uint number = 0;
number++;
// `number` is now `1`
pragma solidity ^0.4.25;
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;
// start here
zombieToOwner[id]=msg.sender;
ownerZombieCount[msg.sender]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
In our zombie game, we don't want the user to be able to create unlimited zombies in their army by repeatedly calling createRandomZombie
— it would make the game not very fun.
Let's use require
to make sure this function only gets executed one time per user, when they create their first zombie.
- Put a
require
statement at the beginning ofcreateRandomZombie
. The function should check to make sureownerZombieCount[msg.sender]
is equal to0
, and throw an error otherwise.
pragma solidity ^0.4.25;
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]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
// start here
require(ownerZombieCount[msg.sender]==0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
In the next chapters, we're going to be implementing the functionality for our zombies to feed and multiply. Let's put this logic into its own contract that inherits all the methods from ZombieFactory
.
- Make a contract called
ZombieFeeding
belowZombieFactory
. This contract should inherit from ourZombieFactory
contract.
pragma solidity ^0.4.25;
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]++;
emit NewZombie(id, _name, _dna);
}
function _generateRandomDna(string _str) private view returns (uint) {
uint rand = uint(keccak256(abi.encodePacked(_str)));
return rand % dnaModulus;
}
function createRandomZombie(string _name) public {
require(ownerZombieCount[msg.sender] == 0);
uint randDna = _generateRandomDna(_name);
_createZombie(_name, randDna);
}
}
// Start here
contract ZombieFeeding is ZombieFactory{
}
Now that we've set up a multi-file structure, we need to use import
to read the contents of the other file:
pragma solidity ^0.4.25;
// put import statement here
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
}
pragma solidity ^0.4.25;
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
// Start here
function feedAndMultiply(uint _zombieId,uint _targetDna) public {
require(msg.sender==zombieToOwner[_zombieId]);
Zombie storage myZombie=zombies[_zombieId];
}
}
pragma solidity ^0.4.25;
import "./zombiefactory.sol";
contract ZombieFeeding is ZombieFactory {
function feedAndMultiply(uint _zombieId, uint _targetDna) public {
require(msg.sender == zombieToOwner[_zombieId]);
Zombie storage myZombie = zombies[_zombieId];
// start here
_targetDna=_targetDna % dnaModulus;
uint newDna=(myZombie.dna + _targetDna)/2;
_createZombie("NoName",newDna);
}
}
pragma solidity ^0.4.25;
import "./zombiefactory.sol";
// Create KittyInterface here
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);
}
}
- Constructors:
function Ownable()
is a constructor, which is an optional special function that has the same name as the contract. It will get executed only one time, when the contract is first created. - Function Modifiers:
modifier onlyOwner()
. Modifiers are kind of half-functions that are used to modify other functions, usually to check some requirements prior to execution. In this case,onlyOwner
can be used to limit access so only the owner of the contract can run this function. We'll talk more about function modifiers in the next chapter, and what that weird_;
does. indexed
keyword: don't worry about this one, we don't need it yet.pragma solidity ^0.4.25; /** * @title Ownable * @dev The Ownable contract has an owner address, and provides basic authorization control * functions, this simplifies the implementation of "user permissions". */ contract Ownable { address private _owner; event OwnershipTransferred( address indexed previousOwner, address indexed newOwner ); /** * @dev The Ownable constructor sets the original `owner` of the contract to the sender * account. */ constructor() internal { _owner = msg.sender; emit OwnershipTransferred(address(0), _owner); } /** * @return the address of the owner. */ function owner() public view returns(address) { return _owner; } /** * @dev Throws if called by any account other than the owner. */ modifier onlyOwner() { require(isOwner()); _; } /** * @return true if `msg.sender` is the owner of the contract. */ function isOwner() public view returns(bool) { return msg.sender == _owner; } /** * @dev Allows the current owner to relinquish control of the contract. * @notice Renouncing to ownership will leave the contract without an owner. * It will not be possible to call the functions with the `onlyOwner` * modifier anymore. */ function renounceOwnership() public onlyOwner { emit OwnershipTransferred(_owner, address(0)); _owner = address(0); } /** * @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 { _transferOwnership(newOwner); } /** * @dev Transfers control of the contract to a newOwner. * @param newOwner The address to transfer ownership to. */ function _transferOwnership(address newOwner) internal { require(newOwner != address(0)); emit OwnershipTransferred(_owner, newOwner); _owner = newOwner; } }
函式修飾符
函式修飾符看起來跟函式沒什麼不同,不過關鍵字
modifier
告訴編譯器,這是個modifier(修飾符)
,而不是個function(函式)
。它不能像函式那樣被直接呼叫,只能被新增到函式定義的末尾,用以改變函式的行為。咱們仔細讀讀
onlyOwner
:-
/** * @dev 呼叫者不是‘主人’,就會丟擲異常 */ modifier onlyOwner() { require(msg.sender == owner); _; }
onlyOwner
函式修飾符是這麼用的: -
contract MyContract is Ownable { event LaughManiacally(string laughter); //注意! `onlyOwner`上場 : function likeABoss() external onlyOwner { LaughManiacally("Muahahahaha"); } }
注意
likeABoss
函式上的onlyOwner
修飾符。 當你呼叫likeABoss
時,首先執行onlyOwner
中的程式碼, 執行到onlyOwner
中的_;
語句時,程式再返回並執行likeABoss
中的程式碼。可見,儘管函式修飾符也可以應用到各種場合,但最常見的還是放在函式執行之前新增快速的
require
檢查。因為給函式添加了修飾符
onlyOwner
,使得唯有合約的主人(也就是部署者)才能呼叫它。 -
注意:主人對合約享有的特權當然是正當的,不過也可能被惡意使用。比如,萬一,主人添加了個後門,
允許他偷走別人的殭屍呢?
所以非常重要的是,部署在以太坊上的 DApp,並不能保證它真正做到去中心,你需要閱讀並理解它的原始碼,才能防止其中沒有被部署者惡意植入後門;作為開發人員,如何做到既要給自己留下修復 bug 的餘地,又要儘量地放權給使用者,以便讓他們放心你,從而願意把資料放在你的 DApp 中,這確實需要個微妙的平衡。
-
-
記住,修飾符的最後一行為
_;
,表示修飾符呼叫結束後返回,並執行呼叫函式餘下的部分。 -
We have visibility modifiers that control when and where the function can be called from:
private
means it's only callable from other functions inside the contract;internal
is likeprivate
but can also be called by contracts that inherit from this one;external
can only be called outside the contract; and finallypublic
can be called anywhere, both internally and externally. -
We also have state modifiers, which tell us how the function interacts with the BlockChain:
view
tells us that by running the function, no data will be saved/changed.pure
tells us that not only does the function not save any data to the blockchain, but it also doesn't read any data from the blockchain. Both of these don't cost any gas to call if they're called externally from outside the contract (but they do cost gas if called internally by another function). -
Then we have custom
modifiers
, which we learned about in Lesson 3:onlyOwner
andaboveLevel
, for example. For these we can define custom logic to determine how they affect a function. -