Kontrakty Ethereum Oracle: funkcje kodu solidności

Opublikowany: 2022-03-11

W pierwszym segmencie tego trzyczęściowego odcinka przeszliśmy przez mały samouczek, który dał nam prostą parę kontraktów z wyrocznią. Opisano mechanizmy i procesy konfiguracji (z truflą), kompilowania kodu, wdrażania do sieci testowej, uruchamiania i debugowania; jednak wiele szczegółów kodu zostało przesłoniętych w sposób ręcznie falowany. Tak więc teraz, zgodnie z obietnicą, przyjrzymy się niektórym z tych funkcji językowych, które są unikalne dla rozwoju inteligentnych kontraktów Solidity i unikalne dla tego konkretnego scenariusza kontraktu-wyroczni. Chociaż nie możemy skrupulatnie przyjrzeć się każdemu szczegółowi (zostawię to Tobie w dalszych badaniach, jeśli chcesz), postaramy się trafić na najbardziej uderzające, najciekawsze i najważniejsze cechy kodu.

Aby to ułatwić, polecam otworzyć własną wersję projektu (jeśli ją posiadasz) lub mieć pod ręką kod do wglądu.

Pełny kod w tym momencie można znaleźć tutaj: https://github.com/jrkosinski/oracle-example/tree/part2-step1

Ethereum i Solidność

Solidity nie jest jedynym dostępnym językiem programowania inteligentnych kontraktów, ale myślę, że jest wystarczająco bezpieczny, aby powiedzieć, że jest to najpopularniejszy i ogólnie najbardziej popularny w przypadku inteligentnych kontraktów Ethereum. Z pewnością jest to ten, który ma najpopularniejsze wsparcie i informacje w momencie pisania tego tekstu.

Schemat najważniejszych cech Solidności Ethereum

Solidność jest zorientowana obiektowo i kompletna pod względem Turinga. To powiedziawszy, szybko zdasz sobie sprawę z jego wbudowanych (i w pełni zamierzonych) ograniczeń, które sprawiają, że programowanie inteligentnych kontraktów jest zupełnie inne niż zwykłe hakowanie typu „zróbmy to”.

Wersja solidności

Oto pierwszy wiersz każdego wiersza kodu Solidity:

 pragma solidity ^0.4.17;

Numery wersji, które widzisz, będą się różnić, ponieważ Solidity, wciąż w młodości, szybko się zmienia i ewoluuje. Wersja 0.4.17 to wersja, której użyłem w moich przykładach; najnowsza wersja w momencie tej publikacji to 0.4.25.

Najnowsza wersja, którą teraz czytasz, może być czymś zupełnie innym. Wiele fajnych funkcji jest w trakcie prac (a przynajmniej planowanych) dla Solidity, które omówimy za chwilę.

Oto przegląd różnych wersji Solidity.

Wskazówka dla profesjonalistów: Możesz także określić zakres wersji (choć nie widzę tego zbyt często), na przykład:

 pragma solidity >=0.4.16 <0.6.0;

Funkcje języka programowania Solidity

Solidity ma wiele cech językowych, które są znane większości współczesnych programistów, a także kilka odrębnych i (przynajmniej dla mnie) niezwykłych. Mówi się, że został zainspirowany C++, Pythonem i JavaScriptem — z których wszystkie są mi osobiście dobrze znane, a jednak Solidity wydaje się całkiem odrębny od któregokolwiek z tych języków.

Kontrakt

Plik .sol jest podstawową jednostką kodu. W BoxingOracle.sol zwróć uwagę na 9 wiersz:

 contract BoxingOracle is Ownable {

Ponieważ klasa jest podstawową jednostką logiki w językach obiektowych, kontrakt jest podstawową jednostką logiki w Solidity. Wystarczy na razie uprościć stwierdzenie, że kontrakt jest „klasą” Solidity (dla programistów zorientowanych obiektowo jest to łatwy skok).

Dziedzictwo

Umowy solidności w pełni wspierają dziedziczenie i działają zgodnie z oczekiwaniami; prywatni członkowie umowy nie są dziedziczeni, natomiast chronieni i publiczni są. Przeciążanie i polimorfizm są obsługiwane zgodnie z oczekiwaniami.

 contract BoxingOracle is Ownable {

W powyższym stwierdzeniu słowo kluczowe „is” oznacza dziedziczenie. BoxingOracle dziedziczy po Ownable. W Solidity jest również obsługiwane dziedziczenie wielokrotne. Dziedziczenie wielokrotne jest wskazywane przez rozdzieloną przecinkami listę nazw klas, na przykład:

 contract Child is ParentA, ParentB, ParentC { …

Chociaż (moim zdaniem) nie jest dobrym pomysłem zbytnie zagłębianie się w strukturę swojego modelu dziedziczenia, oto interesujący artykuł na temat Solidności w odniesieniu do tak zwanego Diamentowego Problemu.

Wyliczenia

Wyliczenia są obsługiwane w 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 }

Jak można się spodziewać (nie różni się od znanych języków), każdej wartości wyliczenia jest przypisywana wartość całkowita, zaczynająca się od 0. Jak stwierdzono w dokumentacji Solidity, wartości wyliczenia można konwertować na wszystkie typy liczb całkowitych (np. uint, uint16, uint32, itp.), ale niejawna konwersja nie jest dozwolona. Co oznacza, że ​​muszą być rzucane jawnie (na przykład do uint).

Solidity Docs: Enums Enums samouczek

Struktury

Struktury to inny sposób, podobnie jak wyliczenia, do tworzenia typu danych zdefiniowanego przez użytkownika. Struktury są znane wszystkim programistom C/C++ i starszym ludziom, takim jak ja. Przykład struktury z wiersza 17. BoxingOracle.sol:

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

Uwaga dla wszystkich starych programistów C: „pakowanie” Struct w Solidity to rzecz, ale są pewne zasady i zastrzeżenia. Niekoniecznie zakładaj, że działa tak samo jak w C; sprawdź dokumentację i bądź świadomy swojej sytuacji, aby upewnić się, czy opakowanie pomoże Ci w danym przypadku.

Solidność Pakowanie Struktury

Po utworzeniu struktury mogą być adresowane w kodzie jako natywne typy danych. Oto przykład składni „wystąpienia” typu struktury utworzonego powyżej:

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

Typy danych w Solidity

To prowadzi nas do bardzo podstawowego tematu typów danych w Solidity. Jakie typy danych obsługuje solidność? Solidność jest typowana statycznie iw chwili pisania tego tekstu typy danych muszą być jawnie zadeklarowane i powiązane ze zmiennymi.

Typy danych w Ethereum Solidity

Typy danych solidności

Boole'a

Typy logiczne są obsługiwane pod nazwą bool i wartościami true lub false

Typy numeryczne

Obsługiwane są typy liczb całkowitych, zarówno ze znakiem, jak i bez znaku, od int8/uint8 do int256/uint256 (to odpowiednio 8-bitowe liczby całkowite do 256-bitowych liczb całkowitych). Typ uint jest skrótem dla uint256 (i podobnie int jest skrótem dla int256).

Warto zauważyć, że typy zmiennoprzecinkowe nie są obsługiwane. Dlaczego nie? Cóż, po pierwsze, kiedy mamy do czynienia z wartościami pieniężnymi, zmienne zmiennoprzecinkowe są dobrze znane jako zły pomysł (oczywiście ogólnie), ponieważ wartość może zostać zgubiona. Wartości eteru są podawane w wei, co stanowi 1/1.000.000.000.000.000.000 eteru i musi to być wystarczająca precyzja do wszystkich celów; nie można rozbić eteru na mniejsze części.

Wartości punktów stałych są obecnie częściowo obsługiwane. Według dokumentów Solidity: „Stałe numery punktów nie są jeszcze w pełni obsługiwane przez Solidity. Można je zadeklarować, ale nie można ich przypisać ani od nich.”

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

Uwaga: W większości przypadków najlepiej jest po prostu użyć uint, ponieważ zmniejszenie wielkości zmiennej (na przykład do uint32) może w rzeczywistości zwiększyć koszty gazu, a nie zmniejszyć je, jak można się spodziewać. Jako ogólna zasada używaj uint, chyba że masz pewność, że masz dobry powód, aby zrobić inaczej.

Rodzaje ciągów

Typ danych string w Solidity to zabawny temat; możesz otrzymać różne opinie w zależności od tego, z kim rozmawiasz. W Solidity jest typ danych typu string, to jest fakt. Moja opinia, prawdopodobnie podzielana przez większość, jest taka, że ​​nie oferuje zbyt dużej funkcjonalności. Analiza ciągów, łączenie, zastępowanie, przycinanie, a nawet liczenie długości ciągu: żadna z tych rzeczy, których prawdopodobnie oczekujesz od typu ciągu, nie jest obecna, więc są one Twoim obowiązkiem (jeśli ich potrzebujesz). Niektórzy używają bytes32 zamiast ciągu; to również można zrobić.

Zabawny artykuł o strunach Solidity

Moja opinia: napisanie własnego typu łańcucha i opublikowanie go do ogólnego użytku może być zabawnym ćwiczeniem.

Typ adresu

Być może unikatowe dla Solidity, mamy typ danych adresowych , specjalnie dla adresów portfela Ethereum lub kontraktów. Jest to 20-bajtowa wartość przeznaczona specjalnie do przechowywania adresów o określonym rozmiarze. Ponadto ma członków typu specjalnie dla adresów tego rodzaju.

 address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;

Typy danych adresowych

Typy daty i godziny

Nie ma natywnego typu Date lub DateTime w Solidity per se, jak na przykład w JavaScript. (O nie — Solidność brzmi coraz gorzej z każdym akapitem!?) Daty są adresowane natywnie jako znaczniki czasu typu uint (uint256). Zazwyczaj są one obsługiwane jako znaczniki czasu w stylu Uniksa, w sekundach, a nie w milisekundach, ponieważ znacznik czasu bloku jest znacznikiem czasu w stylu Uniksa. W przypadkach, w których z różnych powodów potrzebujesz dat czytelnych dla człowieka, dostępne są biblioteki typu open source. Możesz zauważyć, że użyłem jednego w BoxingOracle: DateLib.sol. OpenZeppelin ma również narzędzia do dat, a także wiele innych typów ogólnych bibliotek narzędzi (wkrótce przejdziemy do funkcji biblioteki Solidity).

Wskazówka dla profesjonalistów : OpenZeppelin jest dobrym źródłem (ale oczywiście nie jedynym dobrym źródłem) zarówno wiedzy, jak i wstępnie napisanego ogólnego kodu, który może pomóc w budowaniu kontraktów.

Mapowania

Zauważ, że wiersz 11 BoxingOracle.sol definiuje coś, co nazywa się mapowaniem :

 mapping(bytes32 => uint) matchIdToIndex;

Mapowanie w Solidity to specjalny typ danych do szybkiego wyszukiwania; zasadniczo tablica przeglądowa lub podobna do tablicy haszującej, w której zawarte dane znajdują się w samym łańcuchu bloków (gdy mapowanie jest zdefiniowane, tak jak tutaj, jako element klasy). W trakcie realizacji kontraktu możemy dodać dane do mapowania, podobnie jak dodawanie danych do tablicy mieszającej, a później wyszukać dodane przez nas wartości. Zwróć uwagę, że w tym przypadku dane, które dodajemy, są dodawane do samego łańcucha bloków, więc będą się utrzymywać. Jeśli dodamy go do mapowania dzisiaj w Nowym Jorku, za tydzień ktoś w Stambule może go przeczytać.

Przykład dodawania do mapowania, z wiersza 71 BoxingOracle.sol:

 matchIdToIndex[id] = newIndex+1

Przykład odczytu z mapowania, z wiersza 51 BoxingOracle.sol:

 uint index = matchIdToIndex[_matchId];

Pozycje można również usunąć z mapowania. Nie jest używany w tym projekcie, ale wyglądałby tak:

 delete matchIdToIndex[_matchId];

Zwracane wartości

Jak mogłeś zauważyć, Solidity może trochę przypominać JavaScript, ale nie dziedziczy zbyt wiele z luźności typów i definicji JavaScript. Kod kontraktu musi być zdefiniowany w dość ścisły i ograniczony sposób (i jest to prawdopodobnie dobra rzecz, biorąc pod uwagę przypadek użycia). Mając to na uwadze, rozważ definicję funkcji z wiersza 40 w BoxingOracle.sol

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

OK, więc najpierw zróbmy krótki przegląd tego, co tu jest zawarte. function oznacza to jako funkcję. _getMatchIndex to nazwa funkcji (podkreślenie to konwencja, która wskazuje na członka prywatnego — omówimy to później). Pobiera jeden argument o nazwie _matchId (tym razem konwencja podkreślenia jest używana do oznaczania argumentów funkcji) typu bytes32 . Słowo kluczowe private faktycznie sprawia, że ​​element członkowski jest prywatny w zakresie, view informuje kompilator, że ta funkcja nie modyfikuje żadnych danych w łańcuchu bloków, a na koniec: ~~~ zwraca solidność (uint) ~~~

To mówi, że funkcja zwraca uint (funkcja, która returns void po prostu nie ma tutaj klauzuli return). Dlaczego uint jest w nawiasach? Dzieje się tak, ponieważ funkcje Solidity mogą i często zwracają krotki .

Rozważmy teraz następującą definicję z wiersza 166:

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

Sprawdź klauzulę zwrotu na tym! Zwraca jedną, dwie… siedem różnych rzeczy. OK, więc ta funkcja zwraca te rzeczy jako krotkę. Czemu? W trakcie programowania często będziesz musiał zwrócić strukturę (gdyby to był JavaScript, prawdopodobnie chciałbyś zwrócić obiekt JSON). Cóż, w chwili pisania tego tekstu (chociaż w przyszłości może się to zmienić), Solidity nie obsługuje zwracania struktur z funkcji publicznych. Więc zamiast tego musisz zwrócić krotki. Jeśli jesteś facetem od Pythona, możesz już czuć się komfortowo z krotkami. Wiele języków tak naprawdę ich nie obsługuje, przynajmniej nie w ten sposób.

Zobacz wiersz 159, aby zapoznać się z przykładem zwracania krotki jako wartości zwracanej:

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

A jak akceptujemy zwrot wartości czegoś takiego? Możemy to zrobić tak:

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

Alternatywnie możesz wcześniej zadeklarować zmienne w sposób jawny, z ich poprawnymi typami:

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

A teraz zadeklarowaliśmy 7 zmiennych do przechowywania 7 zwracanych wartości, z których możemy teraz korzystać. W przeciwnym razie, zakładając, że chcielibyśmy tylko jednej lub dwóch wartości, możemy powiedzieć:

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

Widzisz, co tam zrobiliśmy? Mamy tylko dwa, które nas interesowały. Sprawdź wszystkie te przecinki. Musimy je dokładnie policzyć!

Import

Wiersze 3 i 4 BoxingOracle.sol dotyczą importu:

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

Jak można się spodziewać, są to importowanie definicji z plików kodu, które istnieją w tym samym folderze projektu kontraktów, co BoxingOracle.sol.

Modyfikatory

Zauważ, że definicje funkcji mają dołączony zestaw modyfikatorów. Po pierwsze, istnieje widoczność: prywatna, publiczna, wewnętrzna i zewnętrzna — widoczność funkcji.

Ponadto zobaczysz słowa kluczowe pure i view . Wskazują one kompilatorowi, jakiego rodzaju zmiany wprowadzi funkcja, jeśli w ogóle. Jest to ważne, ponieważ taka rzecz ma wpływ na ostateczny koszt gazu związany z uruchomieniem funkcji. Zobacz tutaj wyjaśnienie: Solidity Docs.

Wreszcie, to, co naprawdę chcę omówić, to niestandardowe modyfikatory. Spójrz na wiersz 61 BoxingOracle.sol:

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

Zwróć uwagę na modyfikator onlyOwner tuż przed słowem kluczowym „public”. Oznacza to, że tylko właściciel kontraktu może wywołać tę metodę! Chociaż jest to bardzo ważne, nie jest to natywna cecha Solidity (choć może będzie w przyszłości). Właściwie onlyOwner jest przykładem niestandardowego modyfikatora, który sami tworzymy i używamy. Spójrzmy.

Najpierw modyfikator jest zdefiniowany w pliku Ownable.sol, który jak widać zaimportowaliśmy w wierszu 3 BoxingOracle.sol:

 import "./Ownable.sol"

Zwróć uwagę, że w celu wykorzystania modyfikatora BoxingOracle dziedziczy po Ownable . Wewnątrz Ownable.sol, w wierszu 25, możemy znaleźć definicję modyfikatora wewnątrz kontraktu „Ownable”:

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

(Nawiasem mówiąc, ta umowa Ownable została zaczerpnięta z jednego z zamówień publicznych OpenZeppelin.)

Zauważ, że ta rzecz jest zadeklarowana jako modyfikator, co oznacza, że ​​możemy jej użyć tak, jak mamy, do zmodyfikowania funkcji. Zauważ, że mięso modyfikatora to stwierdzenie „wymagaj”. Instrukcje Require są trochę jak potwierdzenia, ale nie służą do debugowania. Jeśli warunek instrukcji require nie powiedzie się, funkcja zgłosi wyjątek. Parafrazując to stwierdzenie „wymagaj”:

 require(msg.sender == owner);

Można powiedzieć, że oznacza to:

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

I faktycznie, w Solidity 0.4.22 i nowszych możemy dodać komunikat o błędzie do tego żądania:

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

Wreszcie w ciekawie wyglądającej linii:

 _;

Podkreślenie jest skrótem dla „Tutaj, wykonaj pełną zawartość zmodyfikowanej funkcji”. W efekcie instrukcja require zostanie wykonana jako pierwsza, a następnie właściwa funkcja. To tak, jakby poprzedzać ten wiersz logiki przed zmodyfikowaną funkcją.

Jest oczywiście więcej rzeczy, które możesz zrobić za pomocą modyfikatorów. Sprawdź dokumenty: Dokumenty.

Biblioteki Solidarności

Istnieje funkcja językowa Solidity znana jako biblioteka . Mamy przykład w naszym projekcie na DateLib.sol.

Wdrożenie Solidity Library!

Jest to biblioteka ułatwiająca obsługę typów dat. Jest importowany do BoxingOracle w wierszu 4:

 import "./DateLib.sol";

I jest używany w wierszu 13:

 using DateLib for DateLib.DateTime;

DateLib.DateTime to struktura, która jest eksportowana z kontraktu DateLib (uwidacznia się jako element członkowski; zobacz wiersz 4 DateLib.sol) i deklarujemy tutaj, że „używamy” biblioteki DateLib dla określonego typu danych. Tak więc metody i operacje zadeklarowane w tej bibliotece będą miały zastosowanie do typu danych, o którym powiedzieliśmy, że powinien. W ten sposób biblioteka jest używana w Solidity.

Aby uzyskać jaśniejszy przykład, sprawdź niektóre biblioteki OpenZeppelin dla liczb, takie jak SafeMath. Można je zastosować do natywnych (numerycznych) typów danych Solidity (podczas gdy tutaj zastosowaliśmy bibliotekę do niestandardowego typu danych) i są szeroko stosowane.

Interfejsy

Podobnie jak w głównych językach obiektowych, obsługiwane są interfejsy. Interfejsy w Solidity są zdefiniowane jako kontrakty, ale treści funkcji są pomijane dla funkcji. Aby zapoznać się z przykładem definicji interfejsu, zobacz OracleInterface.sol. W tym przykładzie interfejs jest używany jako zastępstwo dla umowy oracle, której treść znajduje się w osobnej umowie z osobnym adresem.

Konwencje nazewnictwa

Oczywiście konwencje nazewnictwa nie są regułą globalną; jako programiści wiemy, że możemy stosować się do konwencji kodowania i nazewnictwa, które do nas przemawiają. Z drugiej strony chcemy, aby inni czuli się komfortowo czytając i pracując z naszym kodem, więc pożądany jest pewien stopień standaryzacji.

Przegląd projektu

Więc teraz, gdy omówiliśmy kilka ogólnych funkcji językowych obecnych w plikach kodu, o których mowa, możemy zacząć bardziej szczegółowo przyglądać się samemu kodowi dla tego projektu.

Wyjaśnijmy więc raz jeszcze cel tego projektu. Celem tego projektu jest dostarczenie półrealistycznej (lub pseudorealistycznej) demonstracji i przykładu inteligentnego kontraktu, który wykorzystuje wyrocznię. W istocie jest to po prostu umowa wzywająca do zawarcia innej oddzielnej umowy.

Przypadek biznesowy przykładu można przedstawić w następujący sposób:

  • Użytkownik chce obstawiać zakłady o różnej wielkości na mecze bokserskie, płacąc za nie pieniądze (eterem) i zbierając wygrane, kiedy i jeśli wygrają.
  • Użytkownik dokonuje tych zakładów za pośrednictwem inteligentnej umowy. (W rzeczywistym przypadku użycia byłby to pełny DApp z interfejsem web3; ale badamy tylko stronę kontraktów).
  • Osobny inteligentny kontrakt — wyrocznia — jest utrzymywany przez stronę trzecią. Jego zadaniem jest prowadzenie listy meczów bokserskich z ich aktualnymi stanami (oczekujące, trwające, zakończone itp.) oraz, jeśli zostały zakończone, zwycięzcą.
  • Umowa główna pobiera listy oczekujących meczów z wyroczni i przedstawia je użytkownikom jako dopasowania „z możliwością obstawiania”.
  • Umowa główna akceptuje zakłady do momentu rozpoczęcia meczu.
  • Po rozstrzygnięciu meczu główna umowa dzieli wygrane i przegrane zgodnie z prostym algorytmem, dokonuje podziału i wypłaca wygrane na żądanie (przegrani po prostu tracą całą stawkę).

Zasady obstawiania:

  • Istnieje określony, minimalny zakład (zdefiniowany w wei).
  • Nie ma maksymalnego zakładu; użytkownicy mogą postawić dowolną kwotę powyżej minimum.
  • Użytkownicy mogą stawiać zakłady do czasu, gdy mecz stanie się „w toku”.

Algorytm dzielenia wygranych:

  • Wszystkie otrzymane zakłady są umieszczane w „puli”.
  • Mały procent jest usuwany z puli dla domu.
  • Każdy zwycięzca otrzymuje część puli, wprost proporcjonalną do względnej wielkości ich zakładów.
  • Wygrane są obliczane, gdy tylko pierwszy użytkownik zażąda wyników, po rozstrzygnięciu meczu.
  • Wygrane są przyznawane na żądanie użytkownika.
  • W przypadku remisu nikt nie wygrywa — każdy odzyskuje swoją stawkę, a kasyno nie otrzymuje żadnej części.

BoxingOracle: kontrakt Oracle

Zapewnione główne funkcje

Można powiedzieć, że wyrocznia ma dwa interfejsy: jeden prezentowany „właścicielowi” i opiekunowi umowy oraz jeden prezentowany ogółowi społeczeństwa; to znaczy kontrakty, które konsumują wyrocznię. Opiekun, oferuje funkcjonalność dostarczania danych do umowy, zasadniczo pobierając dane ze świata zewnętrznego i umieszczając je w łańcuchu bloków. Publicznie oferuje dostęp tylko do odczytu do tych danych. Należy zauważyć, że sama umowa ogranicza osobom niebędącym właścicielami możliwość edytowania jakichkolwiek danych, ale dostęp tylko do odczytu do tych danych jest udzielany publicznie bez ograniczeń.

Do użytkowników:

  • Wymień wszystkie dopasowania
  • Wyświetl listę oczekujących meczów
  • Uzyskaj szczegółowe informacje o konkretnym meczu
  • Uzyskaj status i wynik konkretnego meczu

Do właściciela:

  • Wpisz mecz
  • Zmień stan meczu
  • Ustaw wynik meczu

Ilustracja elementów dostępu użytkownika i właściciela

Historia użytkownika:

  • Nowy mecz bokserski jest ogłoszony i potwierdzony na 9 maja.
  • Ja, opiekun kontraktu (być może jestem znaną siecią sportową lub nowym punktem sprzedaży), dodaję nadchodzący mecz do danych wyroczni na blockchainie ze statusem „w toku”. Każdy lub dowolna umowa może teraz wyszukiwać i wykorzystywać te dane w dowolny sposób.
  • Kiedy mecz się rozpoczyna, ustawiam status tego meczu na „w toku”.
  • Po zakończeniu meczu ustawiam status meczu na „zakończony” i modyfikuję dane meczu, aby wskazać zwycięzcę.

Przegląd kodu Oracle

Ta recenzja jest oparta w całości na BoxingOracle.sol; numery wierszy odnoszą się do tego pliku.

W liniach 10 i 11 deklarujemy miejsce przechowywania meczów:

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

matches to po prostu prosta tablica do przechowywania instancji dopasowań, a mapowanie jest po prostu funkcją mapowania unikalnego identyfikatora dopasowania (wartość bytes32) na jego indeks w tablicy, dzięki czemu jeśli ktoś przekaże nam surowy identyfikator dopasowania, możemy użyj tego mapowania, aby go zlokalizować.

W linii 17 została zdefiniowana i wyjaśniona nasza struktura meczowa:

 //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 }

Wiersz 61: Funkcja addMatch jest przeznaczona do użytku wyłącznie przez właściciela umowy; pozwala na dodanie nowego dopasowania do zapisanych danych.

Wiersz 80: Funkcja declareOutcome pozwala właścicielowi kontraktu ustawić mecz jako „zdecydowany”, ustawiając uczestnika, który wygrał.

Linie 102-166: Wszystkie poniższe funkcje mogą być wywoływane publicznie. Są to ogólnie dostępne dane tylko do odczytu:

  • Funkcja getPendingMatches zwraca listę identyfikatorów wszystkich dopasowań, których bieżący stan to „oczekujące”.
  • Funkcja getAllMatches zwraca listę identyfikatorów wszystkich dopasowań.
  • Funkcja getMatch zwraca pełne szczegóły pojedynczego dopasowania określonego przez identyfikator.

Linie 193-204 deklarują funkcje przeznaczone głównie do testowania, debugowania i diagnostyki.

  • Funkcja testConnection tylko testuje, czy jesteśmy w stanie wywołać kontrakt.
  • Funkcja getAddress zwraca adres tego kontraktu.
  • Funkcja addTestData dodaje kilka dopasowań testowych do listy dopasowań.

Zachęcamy do zapoznania się nieco z kodem przed przejściem do kolejnych kroków. Sugeruję ponowne uruchomienie kontraktu Oracle w trybie debugowania (jak opisano w części 1 tej serii), wywołanie różnych funkcji i zbadanie wyników.

BoxingBets: Umowa z klientem

Ważne jest, aby określić, za co odpowiada umowa klienta (umowa na zakłady), a za co nie jest odpowiedzialna. Umowa z klientem nie jest odpowiedzialna za prowadzenie list prawdziwych meczów bokserskich lub deklarowanie ich wyników. „Ufamy” (tak, wiem, jest to delikatne słowo – och – omówimy to w części 3) wyroczni tej służby. Umowa klienta jest odpowiedzialna za przyjmowanie zakładów. Jest odpowiedzialny za algorytm, który dzieli wygrane i przekazuje je na konta zwycięzców na podstawie wyniku meczu (otrzymanego od wyroczni).

Co więcej, wszystko jest oparte na ściąganiu i nie ma żadnych zdarzeń ani pchnięć. Kontrakt pobiera dane z wyroczni. Kontrakt pobiera wynik meczu z wyroczni (w odpowiedzi na żądanie użytkownika), a kontrakt oblicza wygrane i przekazuje je w odpowiedzi na żądanie użytkownika.

Zapewnione główne funkcje

  • Wyświetl wszystkie oczekujące dopasowania
  • Uzyskaj szczegółowe informacje o konkretnym meczu
  • Uzyskaj status i wynik konkretnego meczu
  • Postaw zakład
  • Poproś/otrzymaj wygraną

Przegląd kodu klienta

Ta recenzja jest oparta w całości na BoxingBets.sol; numery wierszy odnoszą się do tego pliku.

Wiersze 12 i 13, pierwsze wiersze kodu w kontrakcie, definiują pewne mapowania, w których będziemy przechowywać dane naszego kontraktu.

Linia 12 odwzorowuje adresy użytkowników na listy identyfikatorów. Jest to mapowanie użytkownika na listę identyfikatorów zakładów, które należą do użytkownika. Tak więc dla dowolnego adresu użytkownika możemy szybko uzyskać listę wszystkich zakładów, które zostały postawione przez tego użytkownika.

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

Linia 13 odwzorowuje unikalny identyfikator meczu na listę instancji zakładów. Dzięki temu możemy dla dowolnego meczu uzyskać listę wszystkich zakładów, które zostały postawione na ten mecz.

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

Linie 17 i 18 dotyczą połączenia z naszą wyrocznią. Najpierw w zmiennej boxingOracleAddr przechowujemy adres kontraktu Oracle (domyślnie ustawiony na zero). Moglibyśmy na stałe zakodować adres wyroczni, ale wtedy nigdy nie bylibyśmy w stanie go zmienić. (Brak możliwości zmiany adresu wyroczni może być dobrą lub złą rzeczą — możemy to omówić w części 3). Następna linia tworzy instancję interfejsu oracle (zdefiniowaną w OracleInterface.sol) i przechowuje ją w zmiennej.

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

Jeśli przejdziesz do wiersza 58, zobaczysz funkcję setOracleAddress , w której ten adres oracle może zostać zmieniony i w której instancja boxingOracle zostanie przywrócona z nowym adresem.

Linia 21 określa naszą minimalną wielkość zakładu w wei. To oczywiście bardzo mała ilość, zaledwie 0,000001 eteru.

 uint internal minimumBet = 1000000000000;

Odpowiednio w wierszach 58 i 66 mamy funkcje setOracleAddress i getOracleAddress . setOracleAddress ma modyfikator onlyOwner , ponieważ tylko właściciel umowy może zmienić wyrocznię na inną wyrocznię (prawdopodobnie nie jest to dobry pomysł, ale omówimy to w części 3). Z drugiej strony funkcja getOracleAddress jest publicznie wywoływana; każdy może zobaczyć, jaka wyrocznia jest używana.

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

W liniach 72 i 79 mamy odpowiednio funkcje getBettableMatches i getMatch . Zauważ, że są to po prostu przekazywanie połączeń do wyroczni i zwracanie wyniku.

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

Bardzo ważna jest funkcja placeBet (linia 108).

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

Uderzającą cechą tego jest payable modyfikator; byliśmy tak zajęci omawianiem ogólnych funkcji języka, że ​​nie dotknęliśmy jeszcze najważniejszej funkcji, jaką jest możliwość wysyłania pieniędzy wraz z wywołaniami funkcji! W zasadzie tak to jest — jest to funkcja, która może przyjąć kwotę pieniędzy wraz z innymi przesłanymi argumentami i danymi.

Potrzebujemy tego tutaj, ponieważ w tym miejscu użytkownik jednocześnie określa, jaki zakład zamierza postawić, ile pieniędzy zamierza przeznaczyć na ten zakład i faktycznie wysyła pieniądze. Umożliwia to modyfikator payable . Przed zaakceptowaniem zakładu wykonujemy kilka sprawdzeń, aby upewnić się, że zakład jest ważny. Pierwsza kontrola w linii 111 to:

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

Kwota przesłanych pieniędzy jest przechowywana w msg.value . Zakładając, że wszystkie kontrole przejdą, w wierszu 123 przeniesiemy tę kwotę na własność wyroczni, przejmując własność tej kwoty od użytkownika i w posiadanie umowy:

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

Wreszcie, w wierszu 136, mamy funkcję pomocniczą testowania/debugowania, która pomoże nam dowiedzieć się, czy kontrakt jest połączony z prawidłową wyrocznią:

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

Zawijanie

I to jest tak daleko, jak ten przykład; po prostu akceptując zakład. Funkcjonalność dzielenia wygranych i wypłat, a także inna logika została celowo pominięta, aby przykład był wystarczająco prosty dla naszego celu, którym jest po prostu zademonstrowanie użycia wyroczni z umową. Ta bardziej kompletna i złożona logika istnieje obecnie w innym projekcie, który jest rozszerzeniem tego przykładu i jest nadal w fazie rozwoju.

Więc teraz lepiej rozumiemy bazę kodu i użyliśmy jej jako narzędzia i punktu wyjścia do omówienia niektórych funkcji językowych oferowanych przez Solidity. Głównym celem tej trzyczęściowej serii jest zademonstrowanie i omówienie wykorzystania kontraktu z wyrocznią. Celem tej części jest nieco lepsze zrozumienie tego konkretnego kodu i wykorzystanie go jako punktu wyjścia do zrozumienia niektórych cech Solidity i rozwoju inteligentnych kontraktów. Celem trzeciej i ostatniej części będzie omówienie strategii i filozofii korzystania z Oracle oraz tego, jak wpisuje się ona koncepcyjnie w model inteligentnego kontraktu.

Dalsze opcjonalne kroki

Gorąco zachęcam czytelników, którzy chcą dowiedzieć się więcej, aby wzięli ten kod i pobawili się nim. Wdrażaj nowe funkcje. Napraw wszelkie błędy. Implementuj niezaimplementowane funkcje (takie jak interfejs płatności). Przetestuj wywołania funkcji. Zmodyfikuj je i przetestuj ponownie, aby zobaczyć, co się stanie. Dodaj interfejs web3. Dodaj możliwość usuwania meczów lub modyfikowania ich wyników (w przypadku pomyłki). A co z odwołanymi meczami? Wprowadź drugą wyrocznię. Oczywiście umowa może korzystać z dowolnej liczby wyroczni, ale jakie to stwarza problemy? Baw się dobrze; to świetny sposób na naukę, a kiedy robisz to w ten sposób (i czerpiesz z tego przyjemność), na pewno zachowasz więcej tego, czego się nauczyłeś.

Przykładowa, niepełna lista rzeczy do wypróbowania:

  • Uruchom zarówno kontrakt, jak i wyrocznię w lokalnej sieci testnet (w truffle, jak opisano w części 1) i wywołaj wszystkie funkcje wywoływane i wszystkie funkcje testowe.
  • Dodaj funkcję obliczania wygranych i wypłacania ich po zakończeniu meczu.
  • Dodaj funkcję zwrotu wszystkich zakładów w przypadku remisu.
  • Dodaj funkcję, aby zażądać zwrotu lub anulowania zakładu przed rozpoczęciem meczu.
  • Dodaj funkcję, aby umożliwić czasami anulowanie meczów (w takim przypadku każdy będzie potrzebował zwrotu pieniędzy).
  • Zaimplementuj funkcję, aby zagwarantować, że wyrocznia, która była na miejscu, gdy użytkownik postawił zakład, jest tą samą wyrocznią, która zostanie użyta do określenia wyniku tego meczu.
  • Zaimplementuj inną (drugą) wyrocznię, która ma kilka różnych funkcji związanych z nią lub prawdopodobnie służy dyscyplinom innym niż boks (pamiętaj, że liczba uczestników i lista pozwala na różne rodzaje sportów, więc nie jesteśmy ograniczeni tylko do boksu) .
  • Zaimplementuj getMostRecentMatch , aby faktycznie zwracała ostatnio dodane dopasowanie lub dopasowanie najbliższe bieżącej dacie pod względem czasu wystąpienia.
  • Implementuj obsługę wyjątków.

Kiedy już zapoznasz się z mechaniką relacji między kontraktem a wyrocznią, w części 3 tej trzyczęściowej serii omówimy niektóre kwestie strategiczne, projektowe i filozoficzne poruszone w tym przykładzie.