Ethereum Oracle Contracts: Caracteristici Solidity Code
Publicat: 2022-03-11În primul segment al acestei trei părți, am parcurs un mic tutorial care ne-a oferit o pereche simplă de contract-cu-oracol. Au fost descrise mecanismele și procesele de configurare (cu trufă), compilare a codului, implementare într-o rețea de testare, rulare și depanare; cu toate acestea, multe dintre detaliile codului au fost ignorate într-o manieră ondulată. Așa că acum, așa cum am promis, vom începe să examinăm unele dintre acele caracteristici ale limbajului care sunt unice pentru dezvoltarea de contracte inteligente Solidity și unice pentru acest scenariu contract-oracol special. Deși nu putem analiza cu minuțiozitate fiecare detaliu (vă voi lăsa asta în studiile voastre ulterioare, dacă doriți), vom încerca să găsim cele mai izbitoare, mai interesante și mai importante caracteristici ale codului.
Pentru a facilita acest lucru, vă recomand să deschideți fie propria versiune a proiectului (dacă aveți una), fie să aveți codul la îndemână pentru referință.
Codul complet în acest moment poate fi găsit aici: https://github.com/jrkosinski/oracle-example/tree/part2-step1
Ethereum și Soliditate
Solidity nu este singurul limbaj de dezvoltare a contractelor inteligente disponibile, dar cred că este suficient de sigur să spun că este cel mai comun și mai popular în general, pentru contractele inteligente Ethereum. Cu siguranță este cea care are cel mai popular suport și informații, la momentul scrierii acestui articol.
Soliditatea este orientată pe obiect și Turing-completă. Acestea fiind spuse, veți realiza rapid limitările încorporate (și complet intenționate), care fac ca programarea prin contracte inteligente să se simtă destul de diferită de hacking-ul obișnuit, să facem acest lucru.
Versiunea Solidity
Iată primul rând din fiecare poem de cod Solidity:
pragma solidity ^0.4.17;
Numerele versiunilor pe care le vedeți vor diferi, deoarece Solidity, încă în tinerețe, se schimbă și evoluează rapid. Versiunea 0.4.17 este versiunea pe care am folosit-o în exemplele mele; cea mai recentă versiune la momentul publicării este 0.4.25.
Cea mai recentă versiune în acest moment pe care o citiți poate fi cu totul diferită. Multe caracteristici frumoase sunt în lucru (sau cel puțin planificate) pentru Solidity, despre care vom discuta în prezent.
Iată o prezentare generală a diferitelor versiuni Solidity.
Sfat profesionist: puteți specifica și o serie de versiuni (deși nu văd acest lucru făcut prea des), astfel:
pragma solidity >=0.4.16 <0.6.0;
Caracteristicile limbajului de programare Solidity
Solidity are multe caracteristici de limbaj care sunt familiare pentru majoritatea programatorilor moderni, precum și unele care sunt distincte și (cel puțin pentru mine) neobișnuite. Se spune că a fost inspirat de C++, Python și JavaScript - toate îmi sunt bine familiare personal, și totuși Solidity pare destul de diferită de oricare dintre aceste limbi.
Contracta
Fișierul .sol este unitatea de bază a codului. În BoxingOracle.sol, rețineți a 9-a linie:
contract BoxingOracle is Ownable {
Deoarece clasa este unitatea de bază a logicii în limbajele orientate pe obiecte, contractul este unitatea de bază a logicii în Solidity. Este suficient să simplificăm deocamdată să spunem că contractul este „clasa” Solidity (pentru programatorii orientați pe obiecte, acesta este un salt ușor).
Moştenire
Contractele de soliditate susțin pe deplin moștenirea și funcționează așa cum v-ați aștepta; membrii cu contract privat nu sunt moșteniți, în timp ce cei protejați și publici sunt. Supraîncărcarea și polimorfismul sunt acceptate așa cum v-ați aștepta.
contract BoxingOracle is Ownable {
În declarația de mai sus, cuvântul cheie „este” denotă moștenirea. BoxingOracle moștenește de la Ownable. Moștenirea multiplă este, de asemenea, acceptată în Solidity. Moștenirea multiplă este indicată de o listă de nume de clase delimitată prin virgulă, astfel:
contract Child is ParentA, ParentB, ParentC { …
În timp ce (în opinia mea) nu este o idee bună să vă complexați prea mult atunci când vă structurați modelul de moștenire, iată un articol interesant despre Soliditate în ceea ce privește așa-numita Problemă Diamond.
Enumări
Enumerările sunt acceptate în 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 }
După cum v-ați aștepta (nu este diferit de limbile familiare), fiecărei valori de enumerare i se atribuie o valoare întreagă, începând cu 0. După cum se precizează în documentele Solidity, valorile de enumerare sunt convertibile în toate tipurile de numere întregi (de exemplu, uint, uint16, uint32, etc.), dar conversia implicită nu este permisă. Ceea ce înseamnă că trebuie să fie turnate în mod explicit (a uint, de exemplu).
Solidity Docs: Enums Enums Tutorial
Structuri
Structurile sunt o altă modalitate, cum ar fi enumerarile, de a crea un tip de date definit de utilizator. Structurile sunt familiare tuturor codificatorilor de bază C/C++ și bătrânilor precum mine. Un exemplu de struct, din linia 17 din 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; }
Notă pentru toți programatorii vechi C: „ambalarea” structurată în Solidity este un lucru, dar există câteva reguli și avertismente. Nu presupuneți neapărat că funcționează la fel ca în C; verificați documentele și fiți conștienți de situația dvs., pentru a vă asigura că ambalarea vă va ajuta sau nu într-un caz dat.
Ambalare cu structura solidă
Odată create, structurile pot fi adresate în codul dvs. ca tipuri de date native. Iată un exemplu de sintaxă pentru „instanciarea” tipului de struct creat mai sus:
Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);
Tipuri de date în soliditate
Acest lucru ne duce la subiectul de bază al tipurilor de date din Solidity. Ce tipuri de date acceptă solidity? Soliditatea este tipizată static și, în momentul scrierii acestei scrieri, tipurile de date trebuie să fie declarate în mod explicit și legate de variabile.
Tipuri de date de soliditate
booleene
Tipurile booleene sunt acceptate sub numele bool și valorile true sau false
Tipuri numerice
Sunt acceptate tipuri de numere întregi, atât semnate cât și nesemnate, de la int8/uint8 la int256/uint256 (adică numere întregi de 8 biți până la numere întregi de 256 de biți, respectiv). Tipul uint este prescurtare pentru uint256 (și, de asemenea, int este prescurtare pentru int256).
În special, tipurile în virgulă mobilă nu sunt acceptate. De ce nu? Ei bine, în primul rând, atunci când se ocupă de valori monetare, variabilele în virgulă mobilă sunt bine cunoscute a fi o idee proastă (în general, desigur), deoarece valoarea poate fi pierdută în aer. Valorile eterului sunt notate cu wei, care este 1/1.000.000.000.000.000.000 de parte dintr-un eter, iar aceasta trebuie să fie suficientă precizie pentru toate scopurile; nu poți descompune un eter în părți mai mici.
Valorile punctului fix sunt parțial acceptate în acest moment. Potrivit documentației Solidity: „Numerele cu puncte fixe nu sunt încă pe deplin acceptate de Solidity. Ele pot fi declarate, dar nu pot fi atribuite sau de la.”
https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9
Notă: în cele mai multe cazuri, cel mai bine este să utilizați doar uint, deoarece scăderea dimensiunii variabilei (la uint32, de exemplu), poate crește costurile cu gazul, mai degrabă decât să le scădeți așa cum v-ați aștepta. Ca regulă generală, utilizați uint, cu excepția cazului în care sunteți sigur că aveți un motiv întemeiat pentru a face altfel.
Tipuri de șiruri
Tipul de date șir în Solidity este un subiect amuzant; este posibil să obțineți opinii diferite în funcție de cine vorbiți. Există un tip de date șir în Solidity, acesta este un fapt. Părerea mea, probabil împărtășită de majoritatea, este că nu oferă prea multe funcționalități. Analizarea șirurilor, concatenarea, înlocuirea, tăierea, chiar și numărarea lungimii șirului: niciunul dintre acele lucruri la care probabil te-ai așteptat de la un tip de șir nu este prezent și, prin urmare, sunt responsabilitatea ta (dacă ai nevoie de ele). Unii oameni folosesc bytes32 în loc de șir; se poate face si asta.
Articol distractiv despre corzile Solidity
Opinia mea: Ar putea fi un exercițiu distractiv să scrieți propriul tip de șir și să-l publicați pentru uz general.
Tip de Adresă
Unic poate pentru Solidity, avem un tip de date de adresă , în special pentru adresele de portofel Ethereum sau de contracte. Este o valoare de 20 de octeți special pentru stocarea adreselor de dimensiunea respectivă. În plus, are membri de tip special pentru adrese de acest fel.
address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;
Tipuri de date pentru adrese
Tipuri DateTime
Nu există un tip nativ Date sau DateTime în Solidity, în sine, așa cum există în JavaScript, de exemplu. (Oh, nu — Soliditatea sună din ce în ce mai rău cu fiecare paragraf!?) Datele sunt adresate nativ ca marcaje temporale de tip uint (uint256). Ele sunt, în general, tratate ca marcaje temporale în stil Unix, în secunde mai degrabă decât în milisecunde, deoarece marcajul temporal al blocului este un marcaj temporal în stil Unix. În cazurile în care aveți nevoie de date care pot fi citite de om din diverse motive, există biblioteci open-source disponibile. S-ar putea să observați că am folosit unul în BoxingOracle: DateLib.sol. OpenZeppelin are, de asemenea, utilități de date, precum și multe alte tipuri de biblioteci de utilitate generale (Vom ajunge la funcția de bibliotecă a Solidity în curând).
Sfat profesionist: OpenZeppelin este o sursă bună (dar desigur nu singura sursă bună) atât pentru cunoștințe, cât și pentru codul generic pre-scris, care vă poate ajuta să vă construiți contractele.
Mapări
Observați că linia 11 din BoxingOracle.sol definește ceva numit mapare :
mapping(bytes32 => uint) matchIdToIndex;
O mapare în Solidity este un tip de date special pentru căutări rapide; în esență, un tabel de căutare sau similar cu un hashtable, în care datele conținute trăiesc pe blockchain-ul în sine (când maparea este definită, așa cum este aici, ca membru al clasei). Pe parcursul execuției contractului, putem adăuga date la mapare, similar cu adăugarea de date la un hashtable, și mai târziu să căutăm acele valori pe care le-am adăugat. Rețineți din nou că, în acest caz, datele pe care le adăugăm sunt adăugate la blockchain-ul propriu-zis, deci vor persista. Dacă îl adăugăm la cartografierea de astăzi din New York, peste o săptămână cineva din Istanbul o poate citi.
Exemplu de adăugare la mapare, din linia 71 din BoxingOracle.sol:
matchIdToIndex[id] = newIndex+1
Exemplu de citire din mapare, din rândul 51 din BoxingOracle.sol:
uint index = matchIdToIndex[_matchId];
Elementele pot fi, de asemenea, eliminate din mapare. Nu este folosit în acest proiect, dar ar arăta astfel:
delete matchIdToIndex[_matchId];
Valori returnate
După cum probabil ați observat, Solidity poate avea o mică asemănare superficială cu Javascript, dar nu moștenește prea mult din caracterul relaxat al tipurilor și definițiilor JavaScript. Un cod de contract trebuie definit într-un mod destul de strict și restrâns (și probabil că acesta este un lucru bun, având în vedere cazul de utilizare). Având în vedere acest lucru, luați în considerare definiția funcției din linia 40 din BoxingOracle.sol
function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }
OK, deci, să facem mai întâi o scurtă prezentare generală a ceea ce este conținut aici. function
o marchează ca funcție. _getMatchIndex
este numele funcției (scorul de subliniere este o convenție care indică un membru privat - vom discuta despre asta mai târziu). Este nevoie de un argument, numit _matchId
(de data aceasta convenția de subliniere este folosită pentru a desemna argumente ale funcției) de tip bytes32
. Cuvântul cheie private
face ca membrul să fie privat în domeniul de aplicare, view
îi spune compilatorului că această funcție nu modifică nicio dată pe blockchain și, în final: ~~~ solidity returns (uint) ~~~
Aceasta spune că funcția returnează un uint (o funcție care returnează void nu ar avea pur și simplu nicio clauză de returns
aici). De ce este uint în paranteze? Asta pentru că funcțiile Solidity pot și adesea returnează tupluri .
Luați în considerare acum următoarea definiție de la linia 166:
function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }
Consultați clauza de returnare a acesteia! Returnează una, două... șapte lucruri diferite. OK, deci, această funcție returnează aceste lucruri ca un tuplu. De ce? În cursul dezvoltării, veți găsi adesea nevoia să returnați o structură (dacă ar fi JavaScript, ați dori să returnați un obiect JSON, probabil). Ei bine, din momentul scrierii acestei scrieri (deși în viitor acest lucru se poate schimba), Solidity nu acceptă returnarea structurilor din funcțiile publice. Deci, trebuie să returnați tupluri. Dacă sunteți un tip Python, este posibil să vă simțiți deja confortabil cu tupluri. Cu toate acestea, multe limbi nu le acceptă cu adevărat, cel puțin nu în acest fel.
Consultați linia 159 pentru un exemplu de returnare a unui tuplu ca valoare returnată:
return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);
Și cum acceptăm valoarea returnată a unui asemenea lucru? Putem proceda astfel:
var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Alternativ, puteți declara variabilele în mod explicit în prealabil, cu tipurile lor corecte:
//declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Și acum am declarat 7 variabile pentru a păstra cele 7 valori returnate, pe care acum le putem folosi. Altfel, presupunând că am dorit doar una sau două dintre valori, putem spune:
//declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);
Vezi ce am făcut acolo? Avem doar cele două care ne-au interesat. Verificați toate acele virgule. Trebuie să le numărăm cu atenție!
Importurile
Liniile 3 și 4 ale BoxingOracle.sol sunt importuri:
import "./Ownable.sol"; import "./DateLib.sol";
După cum probabil vă așteptați, acestea importă definiții din fișiere de cod care există în același folder de proiect de contracte ca BoxingOracle.sol.
Modificatori
Observați că definițiile funcției au atașați o grămadă de modificatori. În primul rând, există vizibilitatea: privată, publică, internă și externă - vizibilitatea funcției.
În plus, veți vedea cuvinte cheie pure
și view
. Acestea indică compilatorului ce fel de modificări va face funcția, dacă există. Acest lucru este important deoarece un astfel de lucru este un factor în costul final al gazului pentru rularea funcției. Vedeți aici pentru explicații: Solidity Docs.
În cele din urmă, ceea ce vreau cu adevărat să discut, sunt modificatorii personalizați. Aruncă o privire la linia 61 din BoxingOracle.sol:
function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {
Observați modificatorul onlyOwner
chiar înainte de cuvântul cheie „public”. Acest lucru indică faptul că numai proprietarul contractului poate apela această metodă! Deși este foarte importantă, aceasta nu este o caracteristică nativă a Solidity (deși poate că va fi în viitor). De fapt, onlyOwner
este un exemplu de modificator personalizat pe care îl creăm noi înșine și îl folosim. Hai să aruncăm o privire.
În primul rând, modificatorul este definit în fișierul Ownable.sol, pe care puteți vedea că l-am importat pe linia 3 din BoxingOracle.sol:
import "./Ownable.sol"
Rețineți că, pentru a folosi modificatorul, am făcut ca BoxingOracle
să moștenească de la Ownable
. În interiorul Ownable.sol, pe linia 25, putem găsi definiția modificatorului în interiorul contractului „Ownable”:
modifier onlyOwner() { require(msg.sender == owner); _; }
(Acest contract Ownable, apropo, este preluat dintr-unul dintre contractele publice ale OpenZeppelin.)
Rețineți că acest lucru este declarat ca un modificator, indicând că îl putem folosi așa cum avem, pentru a modifica o funcție. Rețineți că carnea modificatorului este o declarație „necesară”. Declarațiile Require sunt un fel de afirmații, dar nu pentru depanare. Dacă condiția instrucțiunii require eșuează, atunci funcția va arunca o excepție. Deci, pentru a parafraza această declarație „necesită”:
require(msg.sender == owner);
Am putea spune că înseamnă:
if (msg.send != owner) throw an exception;
Și, de fapt, în Solidity 0.4.22 și versiuni ulterioare, putem adăuga un mesaj de eroare la această declarație require:
require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");
În cele din urmă, în linia cu aspect curios:
_;
Sublinierea este prescurtarea pentru „Aici, executați întregul conținut al funcției modificate”. Deci, de fapt, instrucțiunea require va fi executată mai întâi, urmată de funcția reală. Deci, este ca și cum ați preveni această linie de logică pentru funcția modificată.

Există, desigur, mai multe lucruri pe care le puteți face cu modificatori. Verificați documentele: Documente.
Biblioteci Solidity
Există o caracteristică de limbaj a Solidity cunoscută sub numele de bibliotecă . Avem un exemplu în proiectul nostru la DateLib.sol.
Aceasta este o bibliotecă pentru o mai bună manipulare mai ușoară a tipurilor de date. Este importat în BoxingOracle la linia 4:
import "./DateLib.sol";
Și este folosit la linia 13:
using DateLib for DateLib.DateTime;
DateLib.DateTime
este o structură care este expusă din contractul DateLib (este expus ca membru; vezi linia 4 din DateLib.sol) și declarăm aici că „folosim” biblioteca DateLib pentru un anumit tip de date. Deci metodele și operațiile declarate în acea bibliotecă se vor aplica tipului de date pe care am spus că ar trebui. Așa se folosește o bibliotecă în Solidity.
Pentru un exemplu mai clar, consultați unele dintre bibliotecile OpenZeppelin pentru numere, cum ar fi SafeMath. Acestea pot fi aplicate tipurilor de date native (numerice) Solidity (în timp ce aici am aplicat o bibliotecă unui tip de date personalizat) și sunt utilizate pe scară largă.
Interfețe
Ca și în limbajele principale orientate pe obiecte, interfețele sunt acceptate. Interfețele în Solidity sunt definite ca contracte, dar corpurile de funcție sunt omise pentru funcții. Pentru un exemplu de definiție a unei interfețe, consultați OracleInterface.sol. În acest exemplu, interfața este utilizată ca substitut pentru contractul oracle, al cărui conținut se află într-un contract separat cu o adresă separată.
Convențiile de denumire
Desigur, convențiile de numire nu sunt o regulă globală; ca programatori, știm că suntem liberi să respectăm convențiile de codificare și denumire care ne atrag. Pe de altă parte, vrem ca ceilalți să se simtă confortabil citind și lucrând cu codul nostru, așa că este de dorit un anumit grad de standardizare.
rezumatul proiectului
Deci, acum că am trecut peste câteva caracteristici generale ale limbajului prezente în fișierele de cod în cauză, putem începe să aruncăm o privire mai specifică asupra codului în sine, pentru acest proiect.
Deci, să clarificăm încă o dată scopul acestui proiect. Scopul acestui proiect este de a oferi o demonstrație semi-realistă (sau pseudo-realistă) și un exemplu de contract inteligent care utilizează un oracol. În esență, acesta este doar un contract care solicită un alt contract separat.
Cazul de afaceri al exemplului poate fi prezentat după cum urmează:
- Un utilizator dorește să facă pariuri de dimensiuni diferite pe meciurile de box, plătind bani (eter) pentru pariuri și colectând câștigurile atunci când și dacă câștigă.
- Un utilizator face aceste pariuri printr-un contract inteligent. (Într-un caz de utilizare real, acesta ar fi un DApp complet cu un front-end web3; dar examinăm doar partea contractelor.)
- Un contract inteligent separat – oracolul – este menținut de o terță parte. Sarcina sa este de a menține o listă a meciurilor de box cu stările lor curente (în așteptare, în curs, terminate etc.) și, dacă s-au terminat, câștigătorul.
- Contractul principal primește liste de meciuri în așteptare de la oracol și le prezintă utilizatorilor ca meciuri „pariabile”.
- Contractul principal acceptă pariuri până la începutul unui meci.
- După ce se decide un meci, contractul principal împarte câștigurile și înfrângerile conform unui algoritm simplu, își ia o reducere și plătește câștigurile la cerere (perdanții își pierd pur și simplu întreaga miză).
Regulile de pariuri:
- Există un pariu minim definit (definit în wei).
- Nu există un pariu maxim; utilizatorii pot paria orice sumă pe care o plac peste minimul.
- Utilizatorii pot plasa pariuri până în momentul în care meciul devine „în desfășurare”.
Algoritm de împărțire a câștigurilor:
- Toate pariurile primite sunt plasate într-un „pot”.
- Din oală se scoate un mic procent, pentru casă.
- Fiecare câștigător primește o proporție din pot, direct proporțională cu mărimea relativă a pariurilor sale.
- Câștigurile sunt calculate de îndată ce primul utilizator solicită rezultatele, după ce meciul este decis.
- Câștigurile sunt acordate la cererea utilizatorului.
- În caz de egalitate, nimeni nu câștigă – fiecare își primește miza înapoi, iar casa nu primește nicio reducere.
BoxingOracle: Contractul Oracle
Funcții principale furnizate
Oracolul are două interfețe, ați putea spune: una prezentată „proprietarului” și întreținătorului contractului și una prezentată publicului larg; adică contracte care consumă oracolul. Menținătorul oferă funcționalitate pentru introducerea datelor în contract, în esență luând date din lumea exterioară și introducându-le în blockchain. Pentru public, acesta oferă acces numai în citire la datele menționate. Este important de reținut că contractul în sine restricționează non-proprietarii să editeze orice date, dar accesul numai în citire la acele date este acordat public fără restricții.
Pentru utilizatori:
- Listați toate meciurile
- Listează meciurile în așteptare
- Obține detalii despre o anumită potrivire
- Obțineți starea și rezultatul unui anumit meci
Către proprietar:
- Introduceți o potrivire
- Schimbați starea meciului
- Rezultatul stabilit al meciului
Povestea utilizatorului:
- Un nou meci de box este anunțat și confirmat pentru 9 mai.
- Eu, menținătorul contractului (poate că sunt o rețea sportivă cunoscută sau o nouă desfacere), adaug meciul viitor la datele oracolului de pe blockchain, cu statutul „în așteptare”. Oricine sau orice contract poate acum să interogheze și să folosească aceste date așa cum dorește.
- Când începe meciul, am setat starea meciului la „în desfășurare”.
- Când meciul se termină, am setat starea meciului la „finalizat” și modific datele meciului pentru a desemna câștigătorul.
Oracle Code Review
Această recenzie se bazează în întregime pe BoxingOracle.sol; numerele de rând fac referire la acel fișier.
Pe liniile 10 și 11, declarăm locul nostru de depozitare pentru meciuri:
Match[] matches; mapping(bytes32 => uint) matchIdToIndex;
matches
este doar o simplă matrice pentru stocarea instanțelor de potrivire, iar maparea este doar o facilitate pentru maparea unui ID unic de potrivire (o valoare de bytes32) la indexul său din matrice, astfel încât dacă cineva ne înmânează un ID brut al unei potriviri, să putem utilizați această mapare pentru a o localiza.
Pe linia 17, structura noastră de potrivire este definită și explicată:
//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 }
Linia 61: Funcția addMatch
este destinată utilizării numai de către proprietarul contractului; permite adăugarea unei noi potriviri la datele stocate.
Linia 80: Funcția declareOutcome
permite proprietarului contractului să seteze o potrivire ca „hotărâtă”, stabilind participantul care a câștigat.
Liniile 102-166: Următoarele funcții sunt toate apelabile de către public. Acestea sunt datele numai în citire care sunt deschise publicului în general:
- Funcția
getPendingMatches
returnează o listă cu ID-urile tuturor potrivirilor a căror stare actuală este „în așteptare”. - Funcția
getAllMatches
returnează o listă cu ID-urile tuturor potrivirilor. - Funcția
getMatch
returnează detaliile complete ale unei singure potriviri, specificate prin ID.
Liniile 193-204 declară funcții care sunt în principal pentru testare, depanare și diagnosticare.
- Function
testConnection
testează doar că putem apela la contract. - Funcția
getAddress
returnează adresa acestui contract. - Funcția
addTestData
adaugă o mulțime de potriviri de testare la lista de potriviri.
Simțiți-vă liber să explorați puțin codul înainte de a trece la următorii pași. Vă sugerez să rulați contractul oracle din nou în modul de depanare (așa cum este descris în partea 1 a acestei serii), să apelați diferite funcții și să examinați rezultatele.
BoxingBets: Contractul clientului
Este important să definiți pentru ce este responsabil contractul client (contractul de pariuri) și pentru ce nu este responsabil. Contractul client nu este responsabil pentru menținerea listelor de meciuri reale de box sau declararea rezultatelor acestora. Avem „încredere” (da, știu, există acel cuvânt sensibil – uh oh – vom discuta despre asta în Partea 3) în oracolul acelui serviciu. Contractul clientului este responsabil pentru acceptarea pariurilor. Este responsabil pentru algoritmul care împarte câștigurile și le transferă în conturile câștigătorilor în funcție de rezultatul meciului (așa cum este primit de la oracol).
În plus, totul se bazează pe tragere și nu există evenimente sau impulsuri. Contractul extrage date din oracol. Contractul extrage rezultatul meciului din oracol (ca răspuns la cererea utilizatorului), iar contractul calculează câștigurile și le transferă ca răspuns la cererea utilizatorului.
Funcții principale furnizate
- Listați toate meciurile în așteptare
- Obține detalii despre o anumită potrivire
- Obțineți starea și rezultatul unui anumit meci
- Pune un pariu
- Solicitați/primiți câștiguri
Revizuirea codului clientului
Această recenzie se bazează în întregime pe BoxingBets.sol; numerele de rând fac referire la acel fișier.
Liniile 12 și 13, primele linii de cod din contract, definesc unele mapări în care vom stoca datele contractului nostru.
Linia 12 mapează adresele utilizatorilor cu liste de ID-uri. Aceasta este maparea unui utilizator la o listă de ID-uri de pariuri care aparțin utilizatorului. Deci, pentru orice adresă de utilizator dată, putem obține rapid o listă cu toate pariurile care au fost făcute de acel utilizator.
mapping(address => bytes32[]) private userToBets;
Linia 13 mapează ID-ul unic al meciului la o listă de instanțe de pariu. Cu aceasta, putem, pentru orice meci, să obținem o listă cu toate pariurile care au fost făcute pentru acel meci.
mapping(bytes32 => Bet[]) private matchToBets;
Rândurile 17 și 18 sunt legate de conexiunea la oracolul nostru. În primul rând, în variabila boxingOracleAddr
, stocăm adresa contractului oracol (setat implicit la zero). Am putea codifica adresa oracolului, dar apoi nu am putea să o schimbăm niciodată. (Nu a putea schimba adresa oracolului ar putea fi un lucru bun sau rău – putem discuta despre asta în partea 3). Următoarea linie creează o instanță a interfeței oracolului (care este definită în OracleInterface.sol) și o stochează într-o variabilă.
//boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);
Dacă treceți înainte la linia 58, veți vedea funcția setOracleAddress
, în care această adresă oracol poate fi schimbată și în care instanța boxingOracle
este reinstanțată cu o nouă adresă.
Linia 21 definește dimensiunea minimă a pariului nostru, în wei. Desigur, aceasta este de fapt o cantitate foarte mică, doar 0,000001 eter.
uint internal minimumBet = 1000000000000;
Pe liniile 58 și respectiv 66, avem funcțiile setOracleAddress
și getOracleAddress
. setOracleAddress
are modificatorul onlyOwner
deoarece numai proprietarul contractului poate schimba oracolul cu un alt oracol (probabil nu este o idee bună, dar vom detalia în partea 3). Funcția getOracleAddress
, pe de altă parte, este apelabilă public; oricine poate vedea ce oracol este folosit.
function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....
Pe liniile 72 și 79, avem funcțiile getBettableMatches
și, respectiv, getMatch
. Rețineți că acestea sunt pur și simplu redirecționarea apelurilor către oracol și returnarea rezultatului.
function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....
Funcția placeBet
este una foarte importantă (linia 108).
function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...
O caracteristică izbitoare a acestuia este modificatorul payable
; am fost atât de ocupați să discutăm despre caracteristicile generale ale limbajului, încât nu am atins încă caracteristica importantă centrală de a putea trimite bani împreună cu apelurile de funcții! Practic, asta este - este o funcție care poate accepta o sumă de bani împreună cu orice alte argumente și date trimise.
Avem nevoie de acest lucru aici, deoarece aici utilizatorul definește simultan ce pariu va face, câți bani intenționează să aibă pe acel pariu și, de fapt, trimite banii. Modificatorul payable
permite acest lucru. Înainte de a accepta pariul, facem o grămadă de verificări pentru a ne asigura de validitatea pariului. Prima verificare de pe linia 111 este:
require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");
Suma de bani trimisă este stocată în msg.value
. Presupunând că toate cecurile trec, pe rândul 123, vom transfera acea sumă în proprietatea oracolului, luând proprietatea asupra acelei sume de la utilizator și în posesia contractului:
address(this).transfer(msg.value);
În cele din urmă, pe linia 136, avem o funcție de ajutor de testare/depanare care ne va ajuta să știm dacă contractul este sau nu conectat la un oracol valid:
function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }
Încheierea
Și asta este de fapt în ceea ce privește acest exemplu; doar acceptând pariul. Funcționalitatea de împărțire a câștigurilor și de plată, precum și o altă logică a fost omisă intenționat pentru a menține exemplul suficient de simplu pentru scopul nostru, care este pur și simplu de a demonstra utilizarea unui oracol cu un contract. Acea logică mai completă și mai complexă există în prezent într-un alt proiect, care este o extensie a acestui exemplu și este încă în dezvoltare.
Așa că acum înțelegem mai bine baza de cod și l-am folosit ca vehicul și punct de plecare pentru a discuta unele dintre caracteristicile lingvistice oferite de Solidity. Scopul principal al acestei serii din trei părți este de a demonstra și discuta despre utilizarea unui contract cu un oracol. Scopul acestei părți este de a înțelege un pic mai bine acest cod specific și de a-l folosi ca punct de îmbarcare în înțelegerea unor caracteristici ale Solidity și dezvoltarea unui contract inteligent. Scopul celei de-a treia și ultimei părți va fi de a discuta despre strategia și filozofia utilizării oracolului și modul în care aceasta se încadrează conceptual în modelul de contract inteligent.
Alți pași opționali
Aș încuraja cu căldură cititorii care doresc să afle mai multe, să ia acest cod și să se joace cu el. Implementați funcții noi. Remediați orice erori. Implementați funcții neimplementate (cum ar fi interfața de plată). Testați apelurile de funcție. Modificați-le și retestați pentru a vedea ce se întâmplă. Adăugați un front-end web3. Adăugați o facilitate pentru eliminarea meciurilor sau modificarea rezultatelor acestora (în cazul unei greșeli). Dar meciurile anulate? Implementați un al doilea oracol. Desigur, un contract este liber să folosească câte oracole dorește, dar ce probleme implică asta? Distreaza-te cu el; aceasta este o modalitate grozavă de a învăța, iar atunci când o faci în acest fel (și te bucuri de el) ești sigur că vei reține mai mult din ceea ce ai învățat.
Un exemplu de listă necuprinzătoare de lucruri de încercat:
- Rulați atât contractul, cât și oracolul în rețeaua de testare locală (în trufă, așa cum este descris în partea 1) și apelați toate funcțiile apelabile și toate funcțiile de testare.
- Adaugă funcționalitate pentru calcularea câștigurilor și achitarea lor, la finalizarea unui meci.
- Adăugați funcționalitate pentru rambursarea tuturor pariurilor în caz de egalitate.
- Adăugați o funcție pentru a solicita o rambursare sau anularea unui pariu, înainte de începerea meciului.
- Adăugați o funcție pentru a permite faptul că potrivirile pot fi uneori anulate (caz în care toată lumea va avea nevoie de o rambursare).
- Implementați o funcție pentru a garanta că oracolul care era în vigoare atunci când un utilizator a plasat un pariu este același oracol care va fi folosit pentru a determina rezultatul meciului respectiv.
- Implementați un alt (al doilea) oracol, care are unele caracteristici diferite asociate cu acesta sau, eventual, servește unui alt sport decât boxul (rețineți că participanții numără și lista permite diferite tipuri de sporturi, deci nu suntem de fapt restricționați doar la box) .
- Implementați
getMostRecentMatch
astfel încât să returneze de fapt fie cea mai recentă potrivire adăugată, fie potrivirea care se apropie cel mai mult de data curentă în ceea ce privește momentul în care va avea loc. - Implementați gestionarea excepțiilor.
Odată ce sunteți familiarizat cu mecanica relației dintre contract și oracol, în partea a treia a acestei serii de trei părți, vom discuta câteva dintre problemele strategice, de design și filozofice ridicate de acest exemplu.