--- tags: it 鐵人賽 30 天, Web 3, ethereum --- # 從以太坊白皮書理解 web 3 概念 - Day9 從今天開始到 Day 22 共 13 天 將會以 [CryptoZombie](https://cryptozombies.io/en/course/) 這個 solidity 學習網站 來了解 Solidity 這個語言 學完之後將能夠知道如何透過 Solidity 來開發 Small Contract ## Solidity 簡介 Solidity 語言是 EVM 上能夠運行的一種腳本語言,用來撰寫 Small Contract 在這 13 天的介紹中 將分為成以下大章節: Smart Contract 基礎資料結構與 Web3.js 互動介面:Day 9 - Day 13 Smart Contract 與去中心化 Oracle 互動: Day 14 Smart Contract 測試與如何建立 Oracle: Day 15 - Day 19 Smart Contract 周邊系統: Day 20 - Day 21 ## Learn Solidity - day 1 建立 ZombieContract 可以開啟另一個視窗跟著本文一起去實作 課程連結如下: [Lession 1: ZombieFactory](https://cryptozombies.io/en/lesson/1/chapter/1) 在開始撰寫 Contract 之前 先理解要實作的需求 ### ZombieContract 需求 ZombieContract 是一個生成 Zombie 物件的工廠 每個 Zombie 具有 name 與 dna 屬性 並且具有一個識別子 id 作為一個辨識生成過的編號 name 是一個隨機字串 dna 是由 id 與 name 做雜湊產生出來的字串 根據這些參數每個 Zombie 物件有都是有他獨特的樣貌 ### Contract 基礎要件 #### Solidity 編譯版本號 在每個 Contract 的代碼前都必須要標注編譯的 Solidity 版本號 因為每個版本都有其特別的功能及不同的資料結構支援 舉例來說: 要撰寫一個由 solidity 版本於 0.5.0 及 0.6.0 之間的 Contract 必須在上方宣告如下: ```solidity= pramga solidity >=0.5.0 <0.6.0; ``` #### Contract 本體 Contract 本體如同物件導向語言裏面,對於物件會宣告一個建立物件的類別 當作建立物件的藍本。 Contract 也是一樣,會宣告一個開頭為 contract 帶有名稱的腳本區段 當作建立該 Contract 的腳本。 如下: 建立一個 ZombieFactory 的空腳本 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory {} ``` #### 狀態變數與整數 在 solidity 裏面,當在 contract 內部宣告一個變數時 這個變數將會永遠寫入 constract 的儲存空間內。代表狀態變數會被寫入區塊鏈上。 舉例來說: ```solidity= contract Example { // myUnsignedInteger 會變成 Contract 內容寫入鏈上 uint myUnsignedInteger = 100; } ``` #### Unsigned Integers 在 solidity , uint 代表這個整數沒有負號,只能存正數。 另外,uint 代表 uint256 的別名。 其他 bit 位數的無號數還有 uint8, uint16, uint32 等等。 在 ZombieFactory 內,需要一個無號數來存儲 dnaDigits 並且設定為 16。 到此 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; } ``` #### 數學運算符號 在 solidity , 數值運算與一般程式語言一樣: * 加法: x+y * 減法: x-y * 乘法: x*y * 除法: x/y * 取餘數: x%y (e.g., 13 % 5 = 3) 另外也支援指數運算 ```solidity= uint x = 5**2; // x = 5*5 = 25 ``` 為了保證 Zombie 的 DNA 字元只有 16 個字元長 所以建立一個 uint 變數 dnaModulus = dnaDigits的16次方 這樣每次只要把數值 % dnaModulus 就會滿足這個條件 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; } ``` #### Structs 資料結構 在 solidity 除了基礎的資料結構外,也支援複合型資料結構 透過 struct 來做宣告 舉例如下: ```solidity= struct Person { uint age; string name; } ``` 其中 string 是儲存 UTF-8 編碼的字串,也就是每個字元只有 8-bit。 因為每個 Zombie 需要有屬於自己的資料,也就是 dna 跟 name 所以需要建立一個 struct 名稱設定為 Zombie Zombie 具有兩個屬性: 1. name: 字串代表其名字 2. dna: uint 代表其特徵值 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; struct Zombie { string name; uint dna; } } ``` #### Arrays 當要存儲大量相同類型的資料時 就可以使用 array 在 Solidity ,根據陣列長度是否為固定分成 fixed 與 dynamic ```solidity= // fixed array 資料集長度固定 uint[2] fixedArray; // dynamic array 資料集長度不固定 uint[] dynamicArray; ``` #### Public 屬性 狀態資料可以把其存取屬性設定為 public , solidity 會自動替這個屬性產生 getter,讓其他有用到的地方讀取。 語法如下 ```solidity= Person[] public people; ``` 在 ZombieFactory ,需要使用一個 dynamic Array 來紀錄所有產生出來的 Zombie 物件,並且需要讓這個 dynamic Array 存取權限是 public。 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; struct Zombie { string name; uint dna; } Zombie[] public zombies; } ``` #### 函式宣告 函式宣告語法如下 ```solidity= function eatHamburgers(string memory _name, uint _amount) public {} ``` 函式宣告包含元素如下: 1. 函式名稱: 如上面函式名稱是 eatHamburgers 2. 參數: 參數根據其傳遞方式分為兩種。 pass by value:傳值,只會把值做複製傳入,在函式中修改不影響其原本的值。 pass by reference:傳參考,會把該參數的參考指標傳入,在函式中修改其值,會透過參考指標改到原本的值,對於參考值需要加入 memory 在參數前面。而參考類型比如 string, structs, mapping, 還有 arrays都需要。 3. 存取權限:這邊的 eatHamburgers 是 public,代表可以直接從 abi 呼叫這個 function 特別注意的是,習慣上的命名規則會把傳入參數以底線當作開頭。 ZombieFactory 需要加入一個 public function 名稱叫作 createZombie。 createZombie 需要兩個參數一個是字串 _name,一個是 uint _dna 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; struct Zombie { string name; uint dna; } Zombie[] public zombies; function createZombie(string memory _name, uint _dna) public { } } ``` #### 使用 Struct and Arrays 在 solidity 中, 產生 struct 的語法如下: ```solidity= struct Person { uint age; string name; } // 產生一個新的 Person Person satoshi = Person(20, "Satoshi"); ``` array 可以透過 array.push 新增元素到 array 之內 舉例如下 ```solidity= Person[] public people; people.push(Person(20, "Satoshi")); ``` #### 加入 createZombie 邏輯 createZombie 會產生一個新的 Zombie,並且把這個 Zombie 加入 zombies array。 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; struct Zombie { string name; uint dna; } Zombie[] public zombies; function createZombie(string memory _name, uint _dna) public { zombies.push(Zombie(_name, _dna)); } } ``` #### public/ private function solidity 對於 function, 有 public 與 private 權限。 在 solidity , function 預設都是 public。代表所有人都可以透過 abi 來呼叫 function 。 然而這樣會很容易把一些能夠修改資料的邏輯給透露給所有人,並非是一種安全的設計。 因此比較好的方式,把 function 預設設定為 private 限定只有 contract 物件可以呼叫。 只把一些需要外部互動的部份給設定成 public。 如下: ```solidity= uint[] numbers; function _addToArray(uint _number) private { numbers.push(_number); } ``` 通常習慣把 private 函式命名以底現作為開頭。 #### 更新 createZombie 為 private 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; struct Zombie { string name; uint dna; } Zombie[] public zombies; function _createZombie(string memory _name, uint _dna) private { zombies.push(Zombie(_name, _dna)); } } ``` #### function 回傳值與修飾子 ##### function 回傳值 讓 function 具有回傳值的語法如下: ```solidity= string greeting = "What's up dog"; function sayHello() public returns (string memory) { return greeting; } ``` ##### function 修飾子 1. view: 代表該 function 只會讀取參考值,並不去做修改。 如下: ```solidity= string greeting = "What's up dog"; function sayHello() public view returns (string memory) { return greeting; } ``` 2. pure: 代表該 function 只做純量運算,不會去存取任何參考值。 如下: ```solidity= function _multiply(uint _a, uint _b) private pure returns (uint) { return _a * _b; } ``` #### 建立 _generateRandomDna function 每個 Zombie 具有一個特徵值 dna ZombieFactory 會需要一個產生隨機數的 function 來處理這個功能。 1. 建立一個 private function 名稱設定為 _generateRandomDna。_generateRandomDna 具有一個字串參數 _str,並且具有一個 uint 回傳值。 2. _generateRandomDna 不能修改 Contract 其他值,需要加入 view 修飾子。 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; 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 與型別轉換 ##### Keccak256 Ethereum 內建 keccak256 函式是一個用來做雜湊值的函數,把輸入資料對應到 256-bit 的 16進位數值。 輸入必須是 bytes,所以需要把輸入字串透過 abi.encodedPacked轉換成 byte。如下: ```solidity= //6e91ec6b618bb462a4a6ee5aa2cb0e9cf30f7a052bb467b0ba58b8748c00d2e5 keccak256(abi.encodePacked("aaaab")); //b1f078126895a1424524de5321b339ab00408010b7cf0e6ed451514981e58aa9 keccak256(abi.encodePacked("aaaac")); ``` ##### 型別轉換 型別轉換是用來轉換兩種不同型別,語法如下 ```solidity= 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); ``` #### 更新 _generateRandomDna 1. 透過 abi.encodePacked(_str) 把輸入 string 產生 256 bit hexicimal string 再透過行別轉換成 uint 並且存入一個 uint 變數 rand 2. 把這個 rand % dnaModulus 在回傳 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; 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; } } ``` #### 新增 createRandomZombie 1. 建立一個 public function,設定名稱叫作 createRandomZombie 。 createRandomZombie 需要一個字串參數 _name。 2. 把 _generateRandomDna 的值存在一個 uint 變數 randDna。 3. 把 _name 跟 randDna 傳入 _createZombie 函數。 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; 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; } function createRandomZombie(string memory _name) public { uint randDna = _generateRandomDna(_name); _createZombie(_name, randomDna); } } ``` #### Events Events 是一種讓 Contract 來根據 blockchain 資料變動來與前端應用互動的方式,透過這種方式可以監聽一些特定的變動。 如下: ```solidity= // declare the event event IntegersAdded(uint x, uint y, uint result); function add(uint _x, uint _y) public returns (uint) { uint result = _x + _y; // fire an event to let the app know the function was called: emit IntegersAdded(_x, _y, result); return result; } ``` 前端應用就可以透過以下語法來監聽: ```javascript= YourContract.IntegersAdded(function(error, result) { // do something with result }) ``` #### 新增 NewZombie Event 1. 建立一個 NewZombie Event,並且傳遞資料 zombieId(uint), name(string), 還有 dna(uint)。 2. 新增驅動 NewZomie 到 _createZombie 邏輯裡,當新的 Zombie 物件加入 zombie 之後。 3. 需要透過存儲 array.push 的回傳值到一個 uint 變數 id 用來當作 NewZombie 的參數。 更新 ZombieFactory 如下 ```solidity= pramga solidity >=0.5.0 <0.6.0; contract ZombieFactory { event NewZombie(uint zombieId, string name, uint dna); uint dnaDigits = 16; uint dnaModulus = dnaDigits**16; 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; 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, randomDna); } } ``` 到這裡,完整的 ZombieFactory 邏輯完成了 當把整個 ZombieFactory Contract Deploy 到已太鏈上時, 我們可以透過產生出來的 abi 使用 web3.js 從前端去呼叫 contract 來產生隨機 Zombie。