以太坊預言機合約:Solidity 代碼特性

已發表: 2022-03-11

在這三部分的第一部分中,我們完成了一個小教程,它為我們提供了一個簡單的合約與預言機對。 描述了設置(使用 truffle)、編譯代碼、部署到測試網絡、運行和調試的機制和過程; 然而,代碼的許多細節都被人為地掩蓋了。 所以現在,正如所承諾的,我們將研究一些語言特性,這些特性是 Solidity 智能合約開發所獨有的,也是這種特定合約預言機場景所獨有的。 雖然我們不能煞費苦心地查看每一個細節(如果您願意,我會在您的進一步研究中將其留給您),但我們將嘗試找出代碼中最引人注目、最有趣和最重要的特性。

為了促進這一點,我建議您打開自己的項目版本(如果有的話),或者將代碼放在手邊以供參考。

此時的完整代碼可以在這裡找到:https://github.com/jrkosinski/oracle-example/tree/part2-step1

以太坊和 Solidity

Solidity 並不是唯一可用的智能合約開發語言,但我認為可以肯定地說,它是最常見和最流行的以太坊智能合約。 當然,在撰寫本文時,它是擁有最受歡迎的支持和信息的那個。

以太坊 Solidity 關鍵特性圖

Solidity 是面向對象和圖靈完備的。 也就是說,你很快就會意識到它的內置(並且完全是有意的)限制,這使得智能合約編程感覺與普通的讓我們做這件事的黑客行為完全不同。

Solidity 版本

這是每首 Solidity 代碼詩的第一行:

 pragma solidity ^0.4.17;

您看到的版本號會有所不同,因為 Solidity 仍處於年輕階段,正在迅速變化和發展。 0.4.17 版是我在示例中使用的版本; 本出版物發佈時的最新版本是 0.4.25。

此時您正在閱讀的最新版本可能完全不同。 Solidity 的許多不錯的功能正在開發中(或至少計劃中),我們現在將討論這些功能。

這是不同 Solidity 版本的概述。

專業提示:您還可以指定一系列版本(儘管我不經常看到這樣做),如下所示:

 pragma solidity >=0.4.16 <0.6.0;

Solidity 編程語言特性

Solidity 具有大多數現代程序員熟悉的許多語言特性,以及一些獨特且(至少對我而言)不尋常的語言特性。 據說它受到了 C++、Python 和 JavaScript 的啟發——我個人對所有這些都很熟悉,但 Solidity 似乎與這些語言中的任何一種都截然不同。

合同

.sol 文件是代碼的基本單元。 在 BoxingOracle.sol 中,注意第 9 行:

 contract BoxingOracle is Ownable {

由於類是面向對象語言中的基本邏輯單元,因此契約是 Solidity 中的基本邏輯單元。 現在簡單地說契約是 Solidity 的“類”就足夠了(對於面向對象的程序員來說,這是一個簡單的飛躍)。

遺產

Solidity 合約完全支持繼承,它可以按你的預期工作; 私人合同成員不被繼承,而受保護和公共的成員是。 如您所料,支持重載和多態。

 contract BoxingOracle is Ownable {

在上面的語句中,“is”關鍵字表示繼承。 BoxingOracle 繼承自 Ownable。 Solidity 也支持多重繼承。 多重繼承由一個以逗號分隔的類名列表表示,如下所示:

 contract Child is ParentA, ParentB, ParentC { …

雖然(在我看來)在構建繼承模型時過於復雜並不是一個好主意,但這裡有一篇關於 Solidity 的關於所謂的鑽石問題的有趣文章。

枚舉

Solidity 支持枚舉:

 enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg, cancelled) Decided //index of participant who is the winner }

正如您所期望的(與熟悉的語言沒有什麼不同),每個枚舉值都被分配一個整數值,從 0 開始。如 Solidity 文檔中所述,枚舉值可以轉換為所有整數類型(例如,uint、uint16、uint32、等),但不允許隱式轉換。 這意味著它們必須顯式轉換(例如,轉換為 uint)。

Solidity 文檔:枚舉枚舉教程

結構

結構是另一種創建用戶定義數據類型的方式,如枚舉。 所有 C/C++ 基礎編碼人員和像我這樣的老傢伙都熟悉結構。 BoxingOracle.sol 第 17 行的結構示例:

 //defines a match along with its outcome struct Match { bytes32 id; string name; string participants; uint8 participantCount; uint date; MatchOutcome outcome; int8 winner; }

所有老 C 程序員請注意: Solidity 中的結構“打包”是一回事,但有一些規則和注意事項。 不一定假設它與 C 中的工作方式相同; 檢查文檔並了解您的情況,以確定在特定情況下包裝是否會對您有所幫助。

Solidity 結構封裝

創建後,結構可以在您的代碼中作為本機數據類型進行尋址。 這是上面創建的結構類型“實例化”的語法示例:

 Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);

Solidity 中的數據類型

這將我們帶到了 Solidity 中數據類型的基本主題。 Solidity 支持哪些數據類型? Solidity 是靜態類型的,在撰寫本文時,數據類型必須顯式聲明並綁定到變量。

以太坊 Solidity 中的數據類型

Solidity 數據類型

布爾值

在名稱bool和值truefalse下支持布爾類型

數值類型

支持整數類型,有符號和無符號,從 int8/uint8 到 int256/uint256(分別是 8 位整數到 256 位整數)。 類型 uint 是 uint256 的簡寫(同樣,int 是 int256 的簡寫)。

值得注意的是,支持浮點類型。 為什麼不? 好吧,一方面,在處理貨幣價值時,眾所周知,浮點變量是一個壞主意(當然,一般來說),因為價值可能會化為烏有。 以太幣值以 wei 表示,即以太幣的 1/1,000,000,000,000,000,000 分之一,對於所有用途而言,它必須具有足夠的精度; 你不能將以太分解成更小的部分。

目前部分支持定點值。 根據 Solidity 文檔: “Solidity 尚未完全支持定點數。 它們可以被聲明,但不能被分配給或來自。”

https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9

注意:在大多數情況下,最好只使用 uint,因為減小變量的大小(例如 uint32)實際上會增加 gas 成本,而不是像您預期的那樣減少它們。 作為一般經驗法則,除非您確定有充分的理由不這樣做,否則請使用 uint。

字符串類型

Solidity 中的字符串數據類型是一個有趣的話題; 根據與誰交談,您可能會得到不同的意見。 Solidity 中有一個字符串數據類型,這是事實。 我的觀點,可能大多數人都同意,是它沒有提供太多功能。 字符串解析、連接、替換、修剪,甚至計算字符串的長度:您可能期望從字符串類型中獲得的那些東西都不存在,因此它們是您的責任(如果您需要它們)。 有些人使用 bytes32 代替字符串; 這也可以做到。

關於 Solidity 字符串的有趣文章

我的意見:編寫自己的字符串類型並將其發布以供一般使用可能是一個有趣的練習。

地址類型

或許對於 Solidity 來說是獨一無二的,我們有一個地址數據類型,專門用於以太坊錢包或合約地址。 這是一個 20 字節的值,專門用於存儲特定大小的地址。 此外,它還具有專門用於此類地址的類型成員。

 address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;

地址數據類型

日期時間類型

Solidity 本身沒有原生的 Date 或 DateTime 類型,就像在 JavaScript 中一樣。 (哦,不,Solidity 的每段聽起來都越來越糟糕!?)日期本身是作為 uint (uint256) 類型的時間戳來處理的。 它們通常作為 Unix 樣式的時間戳處理,以秒而不是毫秒為單位,因為塊時間戳是 Unix 樣式的時間戳。 如果您發現自己出於各種原因需要人類可讀的日期,則可以使用開源庫。 您可能會注意到我在 BoxingOracle 中使用了一個:DateLib.sol。 OpenZeppelin 還具有日期實用程序以及許多其他類型的通用實用程序庫(我們將很快了解 Solidity 的功能)。

專業提示: OpenZeppelin 是知識和預先編寫的通用代碼的一個很好的來源(但當然不是唯一的好來源),可以幫助您建立合同。

映射

注意 BoxingOracle.sol 的第 11 行定義了一個叫做映射的東西:

 mapping(bytes32 => uint) matchIdToIndex;

Solidity 中的映射是一種用於快速查找的特殊數據類型; 本質上是一個查找表或類似於哈希表,其中包含的數據存在於區塊鏈本身上(當映射被定義為類成員時,就像這裡一樣)。 在合約執行過程中,我們可以向映射添加數據,類似於向哈希表添加數據,然後查找我們添加的那些值。 再次注意,在這種情況下,我們添加的數據被添加到區塊鏈本身,因此它將保持不變。 如果我們今天將它添加到紐約的地圖中,一周後伊斯坦布爾的某個人就可以閱讀它。

從 BoxingOracle.sol 的第 71 行添加到映射的示例:

 matchIdToIndex[id] = newIndex+1

從 BoxingOracle.sol 的第 51 行讀取映射的示例:

 uint index = matchIdToIndex[_matchId];

也可以從映射中刪除項目。 它沒有在這個項目中使用,但它看起來像這樣:

 delete matchIdToIndex[_matchId];

返回值

您可能已經註意到,Solidity 可能與 Javascript 有一些表面上的相似之處,但它並沒有繼承 JavaScript 的類型和定義的鬆散性。 必須以相當嚴格和受限的方式定義合約代碼(考慮到用例,這可能是一件好事)。 考慮到這一點,考慮 BoxingOracle.sol 第 40 行的函數定義

function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }

好的,所以,讓我們首先快速概述一下這裡包含的內容。 function將其標記為函數。 _getMatchIndex是函數名(下劃線是表示私有成員的約定——我們稍後會討論)。 它接受一個名為_matchId的參數(這次使用下劃線約定來表示函數參數),類型為bytes32 。 關鍵字private實際上使成員在作用域上是私有的, view告訴編譯器這個函數不會修改區塊鏈上的任何數據,最後:~~~solidity返回(uint)~~~

這表示該函數返回一個 uint(返回 void 的函數在這裡根本沒有returns子句)。 為什麼 uint 在括號中? 那是因為 Solidity 函數可以並且經常返回tuples

現在考慮第 166 行的以下定義:

 function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }

看看這個的退貨條款! 它返回一、二……七種不同的東西。 好的,所以,這個函數將這些東西作為一個元組返回。 為什麼? 在開發過程中,您經常會發現自己需要返回一個結構體(如果是 JavaScript,您可能希望返回一個 JSON 對象)。 好吧,在撰寫本文時(儘管將來可能會改變),Solidity 不支持從公共函數返回結構。 所以你必須返回元組。 如果你是一個 Python 人,你可能已經習慣了元組。 但是,許多語言並不真正支持它們,至少不是以這種方式。

有關將元組作為返回值返回的示例,請參見第 159 行:

 return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);

我們如何接受這樣的返回值? 我們可以這樣做:

 var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);

或者,您可以使用正確的類型預先顯式聲明變量:

 //declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);

現在我們已經聲明了 7 個變量來保存 7 個返回值,我們現在可以使用它們了。 否則,假設我們只想要其中的一兩個值,我們可以說:

 //declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);

看看我們在那裡做了什麼? 我們只得到了我們感興趣的兩個。查看所有這些逗號。 我們必須仔細計算它們!

進口

BoxingOracle.sol 的第 3 行和第 4 行是導入:

 import "./Ownable.sol"; import "./DateLib.sol";

正如您可能預期的那樣,這些是從與 BoxingOracle.sol 相同的合同項目文件夾中存在的代碼文件中導入定義。

修飾符

請注意,函數定義附加了一堆修飾符。 首先,有可見性:私有的、公共的、內部的和外部的——功能可見性。

此外,您會看到關鍵字pureview 。 這些向編譯器指示函數將進行何種更改(如果有)。 這很重要,因為這樣的事情是運行該功能的最終 gas 成本的一個因素。 有關說明,請參見此處:Solidity Docs。

最後,我真正想討論的是自定義修飾符。 查看 BoxingOracle.sol 的第 61 行:

 function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {

請注意“public”關鍵字之前的onlyOwner修飾符。 這表明只有合約的所有者才能調用該方法! 雖然非常重要,但這並不是 Solidity 的原生特性(儘管將來可能會出現)。 實際上, onlyOwner是我們自己創建和使用的自定義修飾符的一個示例。 我們來看一下。

首先,修飾符在文件 Ownable.sol 中定義,您可以看到我們在 BoxingOracle.sol 的第 3 行導入了該文件:

 import "./Ownable.sol"

請注意,為了使用修飾符,我們讓BoxingOracle繼承自Ownable 。 在 Ownable.sol 的第 25 行,我們可以在“Ownable”合約中找到修飾符的定義:

 modifier onlyOwner() { require(msg.sender == owner); _; }

(順便說一下,這個 Ownable 合約取自 OpenZeppelin 的公共合約之一。)

注意這個東西被聲明為修飾符,表示我們可以照原樣使用它來修改函數。 請注意,修飾符的核心是“require”語句。 Require 語句有點像斷言,但不適用於調試。 如果 require 語句的條件失敗,則該函數將拋出異常。 因此,解釋這個“要求”聲明:

 require(msg.sender == owner);

我們可以說這意味著:

 if (msg.send != owner) throw an exception;

而且,事實上,在 Solidity 0.4.22 及更高版本中,我們可以在該 require 語句中添加一條錯誤消息:

 require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");

最後,在好奇的行中:

 _;

下劃線是“這裡,執行修改後的函數的全部內容”的簡寫。 所以實際上, require 語句將首先執行,然後是實際的函數。 因此,這就像將這行邏輯預先添加到修改後的函數中。

當然,你可以用修飾符做更多的事情。 檢查文檔:文檔。

Solidity 庫

Solidity 有一個語言特性,稱為。 我們在 DateLib.sol 的項目中有一個示例。

Solidity 庫實現!

這是一個更容易處理日期類型的庫。 它在第 4 行被導入 BoxingOracle:

 import "./DateLib.sol";

它在第 13 行使用:

 using DateLib for DateLib.DateTime;

DateLib.DateTime是從 DateLib 合同中導出的結構(它作為成員公開;參見 DateLib.sol 的第 4 行),我們在這裡聲明我們正在“使用”特定數據類型的 DateLib 庫。 因此,在該庫中聲明的方法和操作將適用於我們所說的數據類型。 這就是在 Solidity 中使用庫的方式。

一個更清楚的例子,查看一些 OpenZeppelin 的數字庫,例如 SafeMath。 這些可以應用於本機(數字)Solidity 數據類型(而這裡我們將庫應用於自定義數據類型),並且被廣泛使用。

接口

與主流的面向對象語言一樣,支持接口。 Solidity 中的接口被定義為合約,但函數的函數體被省略了。 有關接口定義的示例,請參見 OracleInterface.sol。 在此示例中,該接口用作預言機合約的替代,其內容駐留在具有單獨地址的單獨合約中。

命名約定

當然,命名約定不是全局規則; 作為程序員,我們知道我們可以自由地遵循吸引我們的編碼和命名約定。 另一方面,我們確實希望其他人能夠舒適地閱讀和使用我們的代碼,因此需要一定程度的標準化。

項目概況

所以現在我們已經了解了相關代碼文件中存在的一些通用語言特性,我們可以開始更具體地查看這個項目的代碼本身。

所以,讓我們再次澄清這個項目的目的。 該項目的目的是提供使用預言機的智能合約的半現實(或偽現實)演示和示例。 從本質上講,這只是一個調用另一個單獨合約的合約。

該示例的商業案例可以表述如下:

  • 用戶想要在拳擊比賽上進行不同規模的投注,為投注支付金錢(以太幣),並在他們獲勝時以及如果他們獲勝時收集他們的獎金。
  • 用戶通過智能合約進行這些投注。 (在實際用例中,這將是一個帶有 web3 前端的完整 DApp;但我們只檢查合同方面。)
  • 一個單獨的智能合約——預言機——由第三方維護。 它的工作是維護一個拳擊比賽列表及其當前狀態(待定、進行中、已完成等),如果已完成,則為獲勝者。
  • 主合約從預言機獲取待定匹配列表,並將這些作為“可投注”匹配呈現給用戶。
  • 主合約在比賽開始前接受投注。
  • 比賽決定後,主合約根據簡單的算法分配輸贏,自己分紅,並根據要求支付獎金(輸家只是失去全部賭注)。

投注規則:

  • 有一個已定義的最小賭注(以 wei 定義)。
  • 沒有最大賭注; 用戶可以在最低限額以上投注任何他們喜歡的金額。
  • 用戶可以下注,直到比賽“進行中”。

分配獎金的算法:

  • 所有收到的賭注都放入“底池”。
  • 一小部分從鍋中取出,用於房子。
  • 每個獲勝者都會獲得一定比例的底池,這與他們下注的相對大小成正比。
  • 在比賽決定後,一旦第一個用戶請求結果,就會計算獎金。
  • 根據用戶的要求獎勵獎金。
  • 在平局的情況下,沒有人獲勝——每個人都拿回了他們的賭注,而且房子沒有削減。

BoxingOracle:Oracle 合同

提供的主要功能

預言機有兩個接口,你可以說:一個呈現給合約的“所有者”和維護者,一個呈現給公眾; 即消耗預言機的合約。 作為維護者,它提供了將數據輸入合約的功能,本質上是從外部世界獲取數據並將其放到區塊鏈上。 對公眾來說,它提供對所述數據的只讀訪問權限。 值得注意的是,合同本身限制非所有者編輯任何數據,但對該數據的只讀訪問權限是公開授予的,不受限制。

對用戶:

  • 列出所有匹配項
  • 列出待處理的匹配項
  • 獲取特定比賽的詳細信息
  • 獲取特定比賽的狀態和結果

致業主:

  • 輸入匹配
  • 更改比賽狀態
  • 設定比賽結果

用戶和所有者訪問元素的插圖

用戶的故事:

  • 5 月 9 日宣布並確認了一場新的拳擊比賽。
  • 我,合約的維護者(也許我是一個知名的體育網絡或新的網點),將即將到來的比賽添加到區塊鏈上的預言機數據中,狀態為“待定”。 任何人或任何合約現在都可以根據自己的喜好查詢和使用這些數據。
  • 比賽開始時,我將該比賽的狀態設置為“進行中”。
  • 當比賽結束時,我將比賽的狀態設置為“完成”並修改比賽數據以表示獲勝者。

甲骨文代碼審查

這篇評論完全基於 BoxingOracle.sol; 行號引用該文件。

在第 10 行和第 11 行,我們聲明了匹配的存儲位置:

 Match[] matches; mapping(bytes32 => uint) matchIdToIndex;

matches只是一個用於存儲匹配實例的簡單數組,映射只是將唯一匹配 ID(bytes32 值)映射到其在數組中的索引的工具,因此如果有人給我們匹配的原始 ID,我們可以使用此映射來定位它。

在第 17 行,我們定義並解釋了匹配結構:

 //defines a match along with its outcome struct Match { bytes32 id; //unique id string name; //human-friendly name (eg, Jones vs. Holloway) string participants; //a delimited string of participant names uint8 participantCount; //number of participants (always 2 for boxing matches!) uint date; //GMT timestamp of date of contest MatchOutcome outcome; //the outcome (if decided) int8 winner; //index of the participant who is the winner } //possible match outcomes enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg, cancelled) Decided //index of participant who is the winner }

第 61 行:函數addMatch僅供合約所有者使用; 它允許為存儲的數據添加新的匹配項。

第 80 行:函數declareOutcome允許合約所有者將匹配設置為“已確定”,設置獲勝的參與者。

第 102-166 行:以下函數都可以被公眾調用。 這是一般向公眾開放的只讀數據:

  • 函數getPendingMatches返回當前狀態為“待定”的所有匹配項的 ID 列表。
  • 函數getAllMatches返回所有匹配項的 ID 列表。
  • 函數getMatch返回由 ID 指定的單個匹配項的完整詳細信息。

第 193-204 行聲明了主要用於測試、調試和診斷的函數。

  • 函數testConnection只是測試我們是否能夠調用合約。
  • 函數getAddress返回此合約的地址。
  • 函數addTestData將一堆測試匹配添加到匹配列表中。

在繼續下一步之前,請隨意探索一下代碼。 我建議在調試模式下再次運行 oracle 合約(如本系列的第 1 部分所述),調用不同的函數並檢查結果。

BoxingBets:客戶合同

定義客戶合同(投注合同)負責什麼以及不負責什麼是很重要的。 客戶合同負責維護真實拳擊比賽的列表或宣布其結果。 我們“信任”(是的,我知道,有一個敏感詞——呃,哦——我們將在第 3 部分中討論)該服務的預言機。 客戶合約負責接受投注。 它負責分配獎金並根據比賽結果(從預言機收到)將獎金轉移到獲勝者賬戶的算法。

此外,一切都是基於拉的,沒有事件或推動。 合約從預言機中提取數據。 合約從預言機中提取匹配結果(響應用戶請求),合約計算獎金並根據用戶請求進行轉移。

提供的主要功能

  • 列出所有待處理的匹配項
  • 獲取特定比賽的詳細信息
  • 獲取特定比賽的狀態和結果
  • 下注
  • 請求/接收獎金

客戶代碼審查

此評論完全基於 BoxingBets.sol; 行號引用該文件。

第 12 行和第 13 行是合約中的第一行代碼,定義了一些映射,我們將在其中存儲合約數據。

第 12 行將用戶地址映射到 ID 列表。 這是將用戶映射到屬於該用戶的投注 ID 列表。 因此,對於任何給定的用戶地址,我們可以快速獲取該用戶已進行的所有投注的列表。

 mapping(address => bytes32[]) private userToBets;

第 13 行將比賽的唯一 ID 映射到下注實例列表。 有了這個,對於任何給定的比賽,我們都可以獲取為該比賽所做的所有投注的列表。

 mapping(bytes32 => Bet[]) private matchToBets;

第 17 和 18 行與我們的 oracle 的連接有關。 首先,在boxingOracleAddr變量中,我們存儲了預言機合約的地址(默認設置為零)。 我們可以對預言機的地址進行硬編碼,但我們永遠無法更改它。 (不能更改預言機的地址可能是好事也可能是壞事——我們可以在第 3 部分討論)。 下一行創建一個 oracle 接口的實例(在 OracleInterface.sol 中定義)並將其存儲在一個變量中。

 //boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);

如果您跳到第 58 行,您將看到setOracleAddress函數,在該函數中可以更改此預言機地址,並且使用新地址重新實例化boxingOracle實例。

第 21 行定義了我們的最小下注規模,單位為 wei。 這當然是一個非常小的數量,只有 0.000001 以太幣。

 uint internal minimumBet = 1000000000000;

分別在第 58 和 66 行,我們有setOracleAddressgetOracleAddress函數。 setOracleAddress具有onlyOwner修飾符,因為只有合約的所有者才能將預言機切換到另一個預言機(可能不是一個好主意,但我們將在第 3 部分中詳細說明)。 另一方面, getOracleAddress函數是可公開調用的; 任何人都可以看到正在使用什麼預言機。

 function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....

在第 72 和 79 行,我們分別有getBettableMatchesgetMatch函數。 請注意,這些只是將調用轉發到 oracle 並返回結果。

 function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....

placeBet函數是一個非常重要的函數(第 108 行)。

 function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...

這個的一個顯著特徵是payable修飾符; 我們一直忙於討論通用語言特性,以至於我們還沒有觸及能夠與函數調用一起匯款的核心重要特性! 基本上就是這樣——它是一個可以接受一定數量的錢以及發送的任何其他參數和數據的函數。

我們在這裡需要這個,因為這是用戶同時定義他們將要進行什麼賭注、他們打算在該賭注上獲得多少錢以及實際匯款的地方。 payable修飾符可以實現這一點。 在接受投注之前,我們會進行一系列檢查以確保投注的有效性。 第 111 行的第一個檢查是:

 require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");

發送的金額存儲在msg.value中。 假設所有檢查都通過,在第 123 行,我們將把該金額轉移到預言機的所有權中,將該金額的所有權從用戶手中奪走,並歸合約所有:

 address(this).transfer(msg.value);

最後,在第 136 行,我們有一個測試/調試輔助函數,它將幫助我們了解合約是否連接到有效的預言機:

 function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }

包起來

這實際上就是這個例子。 只是接受賭注。 分配獎金和支付的功能以及其他一些邏輯被故意省略,以使示例對我們的目的足夠簡單,這只是為了演示如何使用帶有合約的預言機。 更完整、更複雜的邏輯目前存在於另一個項目中,它是這個例子的擴展,還在開發中。

因此,現在我們對代碼庫有了更好的理解,並將其用作討論 Solidity 提供的一些語言特性的載體和起點。 這個由三部分組成的系列的主要目的是演示和討論合約與預言機的使用。 這部分的目的是更好地理解這個特定的代碼,並將其作為理解 Solidity 和智能合約開發的一些特性的起點。 第三部分也是最後一部分的目的是討論預言機使用的策略和理念,以及它如何在概念上適合智能合約模型。

進一步的可選步驟

我強烈鼓勵希望了解更多信息的讀者使用此代碼並使用它。 實施新功能。 修復任何錯誤。 實現未實現的功能(例如支付接口)。 測試函數調用。 修改它們並重新測試以查看會發生什麼。 添加一個 web3 前端。 添加用於刪除匹配項或修改其結果的工具(以防出錯)。 取消的比賽怎麼辦? 實施第二個預言機。 當然,合約可以隨意使用任意數量的預言機,但這會帶來什麼問題呢? 玩得開心; 這是一種很好的學習方式,當你這樣做(並從中獲得樂趣)時,你一定會記住更多你學到的東西。

要嘗試的示例,非全面列表:

  • 在本地測試網中運行合約和預言機(在 truffle 中,如第 1 部分所述)並調用所有可調用函數和所有測試函數。
  • 添加用於計算獎金並在比賽結束時支付獎金的功能。
  • 添加在平局時退還所有投注的功能。
  • 添加一項功能以在比賽開始前請求退款或取消投注。
  • 添加一項功能以允許比賽有時會被取消(在這種情況下,每個人都需要退款)。
  • 實施一項功能,以確保用戶下注時的預言機與將用於確定比賽結果的預言機相同。
  • 實現另一個(第二個)oracle,它具有一些與之相關的不同功能,或者可能服務於拳擊以外的運動(請注意,參與者計數和列表允許不同類型的運動,因此我們實際上並不僅限於拳擊) .
  • 實現getMostRecentMatch以便它實際上返回最近添加的匹配,或者就發生時間而言最接近當前日期的匹配。
  • 實現異常處理。

一旦您熟悉了合約和預言機之間的關係機制,在這個由三部分組成的系列的第 3 部分中,我們將討論這個示例提出的一些戰略、設計和哲學問題。