Kontrak Oracle Ethereum: Fitur Kode Soliditas
Diterbitkan: 2022-03-11Di segmen pertama dari tiga bagian ini, kami melalui sedikit tutorial yang memberi kami pasangan kontrak-dengan-oracle sederhana. Mekanisme dan proses penyiapan (dengan truffle), kompilasi kode, penerapan ke jaringan uji, pengoperasian, dan debugging dijelaskan; namun, banyak detail kode yang dipoles dengan cara bergelombang. Jadi sekarang, seperti yang dijanjikan, kita akan melihat beberapa fitur bahasa yang unik untuk pengembangan kontrak pintar Solidity dan unik untuk skenario oracle kontrak khusus ini. Meskipun kami tidak dapat dengan susah payah melihat setiap detail (saya akan menyerahkannya kepada Anda dalam studi Anda lebih lanjut, jika Anda mau), kami akan mencoba untuk menemukan fitur kode yang paling mencolok, paling menarik, dan paling penting.
Untuk memfasilitasi ini, saya sarankan Anda membuka versi proyek Anda sendiri (jika ada), atau memiliki kode yang berguna untuk referensi.
Kode lengkap pada saat ini dapat ditemukan di sini: https://github.com/jrkosinski/Oracle-example/tree/part2-step1
Ethereum dan Soliditas
Soliditas bukan satu-satunya bahasa pengembangan kontrak pintar yang tersedia, tetapi saya pikir itu cukup aman untuk mengatakan bahwa ini adalah yang paling umum dan paling populer secara umum, untuk kontrak pintar Ethereum. Tentu saja itu yang memiliki dukungan dan informasi paling populer, pada saat penulisan ini.
Soliditas berorientasi objek dan Turing-lengkap. Karena itu, Anda akan segera menyadari keterbatasan bawaannya (dan sepenuhnya disengaja), yang membuat pemrograman kontrak pintar terasa sangat berbeda dari peretasan let's-do-this-thing biasa.
Versi Soliditas
Berikut adalah baris pertama dari setiap puisi kode Soliditas:
pragma solidity ^0.4.17;
Nomor versi yang Anda lihat akan berbeda, karena Soliditas, yang masih muda, berubah dan berkembang dengan cepat. Versi 0.4.17 adalah versi yang saya gunakan dalam contoh saya; versi terbaru pada saat publikasi ini adalah 0.4.25.
Versi terbaru yang saat ini Anda baca mungkin merupakan sesuatu yang sama sekali berbeda. Banyak fitur bagus sedang dalam pengerjaan (atau setidaknya direncanakan) untuk Solidity, yang akan kita bahas saat ini.
Berikut adalah ikhtisar dari berbagai versi Soliditas.
Kiat pro: Anda juga dapat menentukan rentang versi (meskipun saya tidak melihat ini dilakukan terlalu sering), seperti:
pragma solidity >=0.4.16 <0.6.0;
Fitur Bahasa Pemrograman Soliditas
Solidity memiliki banyak fitur bahasa yang akrab bagi sebagian besar programmer modern serta beberapa yang berbeda dan (setidaknya bagi saya) tidak biasa. Dikatakan telah terinspirasi oleh C++, Python, dan JavaScript—semuanya sangat familiar bagi saya secara pribadi, namun Solidity tampaknya cukup berbeda dari bahasa-bahasa tersebut.
Kontrak
File .sol adalah unit dasar kode. Di BoxingOracle.sol, perhatikan baris ke-9:
contract BoxingOracle is Ownable {
Karena kelas adalah unit dasar logika dalam bahasa berorientasi objek, kontrak adalah unit dasar logika dalam Soliditas. Cukuplah untuk menyederhanakannya untuk saat ini untuk mengatakan bahwa kontrak adalah "kelas" Soliditas (untuk pemrogram berorientasi objek, ini adalah lompatan yang mudah).
Warisan
Kontrak soliditas sepenuhnya mendukung pewarisan, dan berfungsi seperti yang Anda harapkan; anggota kontrak swasta tidak diwarisi, sedangkan yang dilindungi dan publik. Overloading dan polimorfisme didukung seperti yang Anda harapkan.
contract BoxingOracle is Ownable {
Dalam pernyataan di atas, kata kunci “adalah” menunjukkan pewarisan. BoxingOracle mewarisi dari Ownable. Warisan berganda juga didukung di Solidity. Warisan berganda ditunjukkan oleh daftar nama kelas yang dipisahkan koma, seperti:
contract Child is ParentA, ParentB, ParentC { …
Meskipun (menurut saya) bukanlah ide yang baik untuk terlalu rumit ketika menyusun model pewarisan Anda, inilah artikel menarik tentang Soliditas sehubungan dengan apa yang disebut Masalah Berlian.
enum
Enum didukung di 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 }
Seperti yang Anda harapkan (tidak berbeda dari bahasa yang sudah dikenal), setiap nilai enum diberi nilai integer, dimulai dengan 0. Sebagaimana dinyatakan dalam dokumen Solidity, nilai enum dapat dikonversi ke semua tipe integer (mis., uint, uint16, uint32, dll.), tetapi konversi implisit tidak diperbolehkan. Yang berarti bahwa mereka harus dilemparkan secara eksplisit (untuk uint, misalnya).
Solidity Docs: Enums Enum Tutorial
Struktur
Struct adalah cara lain, seperti enum, untuk membuat tipe data yang ditentukan pengguna. Struct familiar bagi semua pembuat kode dasar C/C++ dan orang-orang lama seperti saya. Contoh struct, dari baris 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; }
Catatan untuk semua programmer C lama: Struct "packing" di Solidity adalah suatu hal, tetapi ada beberapa aturan dan peringatan. Jangan berasumsi bahwa itu bekerja sama seperti di C; periksa dokumen dan waspadai situasi Anda, untuk memastikan apakah pengepakan akan membantu Anda dalam kasus tertentu atau tidak.
Pengemasan Struktur Soliditas
Setelah dibuat, struct dapat dialamatkan dalam kode Anda sebagai tipe data asli. Berikut adalah contoh sintaks untuk "instantiation" dari tipe struct yang dibuat di atas:
Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);
Tipe Data dalam Soliditas
Ini membawa kita ke subjek yang paling dasar dari tipe data di Solidity. Tipe data apa yang didukung soliditas? Soliditas diketik secara statis, dan pada saat penulisan ini tipe data harus dideklarasikan secara eksplisit dan terikat pada variabel.
Tipe Data Soliditas
Boolean
Tipe Boolean didukung dengan nama bool dan nilai true atau false
Tipe numerik
Jenis bilangan bulat didukung, baik yang ditandatangani maupun tidak, dari int8/uint8 hingga int256/uint256 (masing-masing bilangan bulat 8-bit hingga bilangan bulat 256-bit). Jenis uint adalah singkatan untuk uint256 (dan juga int adalah kependekan dari int256).
Khususnya, tipe floating-point tidak didukung. Kenapa tidak? Nah, untuk satu hal, ketika berhadapan dengan nilai moneter, variabel floating-point dikenal sebagai ide yang buruk (secara umum tentu saja), karena nilai dapat hilang begitu saja. Nilai eter dilambangkan dalam wei, yaitu 1/1.000.000.000.000.000.000.000 eter, dan itu harus cukup presisi untuk semua tujuan; Anda tidak dapat memecah eter menjadi bagian-bagian yang lebih kecil.
Nilai titik tetap didukung sebagian saat ini. Menurut dokumen Solidity: “Angka poin tetap belum sepenuhnya didukung oleh Solidity. Mereka dapat dideklarasikan, tetapi tidak dapat ditugaskan ke atau dari.”
https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9
Catatan: Dalam kebanyakan kasus, yang terbaik adalah menggunakan uint saja, karena mengurangi ukuran variabel (menjadi uint32, misalnya), sebenarnya dapat meningkatkan biaya bahan bakar daripada menurunkannya seperti yang Anda harapkan. Sebagai aturan umum, gunakan uint kecuali Anda yakin Anda memiliki alasan yang baik untuk melakukan sebaliknya.
Tipe Tali
Tipe data string di Solidity adalah subjek yang lucu; Anda mungkin mendapatkan pendapat yang berbeda tergantung pada siapa Anda berbicara. Ada tipe data string di Solidity, itu fakta. Pendapat saya, mungkin dimiliki oleh sebagian besar orang, adalah bahwa ia tidak menawarkan banyak fungsi. Penguraian string, penggabungan, ganti, potong, bahkan menghitung panjang string: tidak ada satu pun dari hal-hal yang mungkin Anda harapkan dari tipe string yang ada, jadi itu adalah tanggung jawab Anda (jika Anda membutuhkannya). Beberapa orang menggunakan byte32 sebagai pengganti string; yang dapat dilakukan juga.
Artikel menyenangkan tentang string Soliditas
Pendapat saya: Ini mungkin latihan yang menyenangkan untuk menulis tipe string Anda sendiri dan mempublikasikannya untuk penggunaan umum.
tipe alamat
Unik mungkin untuk Solidity, kami memiliki tipe data alamat , khusus untuk dompet Ethereum atau alamat kontrak. Ini adalah nilai 20 byte khusus untuk menyimpan alamat dengan ukuran tertentu. Selain itu, ia memiliki anggota tipe khusus untuk alamat semacam itu.
address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;
Tipe Data Alamat
Jenis TanggalWaktu
Tidak ada tipe Date atau DateTime asli di Solidity, seperti yang ada di JavaScript, misalnya. (Oh tidak—Soliditas terdengar lebih buruk dan lebih buruk di setiap paragraf!?) Tanggal secara asli dialamatkan sebagai cap waktu tipe uint (uint256). Mereka umumnya ditangani sebagai stempel waktu gaya Unix, dalam hitungan detik, bukan milidetik, karena stempel waktu blok adalah stempel waktu gaya Unix. Jika Anda membutuhkan tanggal yang dapat dibaca manusia karena berbagai alasan, ada perpustakaan sumber terbuka yang tersedia. Anda mungkin memperhatikan bahwa saya telah menggunakan satu di BoxingOracle: DateLib.sol. OpenZeppelin juga memiliki utilitas tanggal serta banyak jenis pustaka utilitas umum lainnya (Kita akan segera masuk ke fitur pustaka Soliditas).
Kiat pro: OpenZeppelin adalah sumber yang baik (tetapi tentu saja bukan satu-satunya sumber yang baik) untuk pengetahuan dan kode generik pra-tertulis yang dapat membantu Anda membangun kontrak.
Pemetaan
Perhatikan bahwa baris 11 dari BoxingOracle.sol mendefinisikan sesuatu yang disebut pemetaan :
mapping(bytes32 => uint) matchIdToIndex;
Pemetaan di Solidity adalah tipe data khusus untuk pencarian cepat; dasarnya tabel pencarian atau mirip dengan hashtable, di mana data yang terkandung hidup di blockchain itu sendiri (ketika pemetaan didefinisikan, seperti di sini, sebagai anggota kelas). Selama pelaksanaan kontrak, kita dapat menambahkan data ke pemetaan, mirip dengan menambahkan data ke tabel hash, dan kemudian mencari nilai-nilai yang telah kita tambahkan. Perhatikan lagi bahwa dalam hal ini, data yang kita tambahkan ditambahkan ke blockchain itu sendiri, sehingga akan tetap ada. Jika kita menambahkannya ke pemetaan hari ini di New York, seminggu dari sekarang seseorang di Istanbul dapat membacanya.
Contoh penambahan ke pemetaan, dari baris 71 dari BoxingOracle.sol:
matchIdToIndex[id] = newIndex+1
Contoh pembacaan dari pemetaan, dari baris 51 BoxingOracle.sol:
uint index = matchIdToIndex[_matchId];
Item juga dapat dihapus dari pemetaan. Itu tidak digunakan dalam proyek ini, tetapi akan terlihat seperti ini:
delete matchIdToIndex[_matchId];
Mengembalikan Nilai
Seperti yang mungkin telah Anda perhatikan, Solidity mungkin memiliki sedikit kemiripan yang dangkal dengan Javascript, tetapi tidak mewarisi banyak jenis dan definisi JavaScript yang longgar. Kode kontrak harus didefinisikan dengan cara yang agak ketat dan terbatas (dan ini mungkin hal yang baik, mengingat kasus penggunaan). Dengan mengingat hal itu, pertimbangkan definisi fungsi dari baris 40 dari BoxingOracle.sol
function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }
Oke, jadi, pertama-tama mari kita lakukan tinjauan singkat tentang apa yang ada di sini. function
menandainya sebagai fungsi. _getMatchIndex
adalah nama fungsi (garis bawah adalah konvensi yang menunjukkan anggota pribadi—kita akan membahasnya nanti). Dibutuhkan satu argumen, bernama _matchId
(kali ini konvensi garis bawah digunakan untuk menunjukkan argumen fungsi) dengan tipe bytes32
. Kata kunci private
sebenarnya membuat anggota menjadi pribadi dalam ruang lingkup, view
memberi tahu kompiler bahwa fungsi ini tidak mengubah data apa pun di blockchain, dan akhirnya: ~~~ pengembalian soliditas (uint) ~~~
Ini mengatakan bahwa fungsi mengembalikan uint (fungsi yang mengembalikan void tidak akan memiliki klausa returns
di sini). Mengapa uint dalam tanda kurung? Itu karena fungsi Solidity dapat dan sering mengembalikan tupel .
Pertimbangkan sekarang, definisi berikut dari baris 166:
function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }
Lihat klausa pengembalian yang satu ini! Ini mengembalikan satu, dua ... tujuh hal yang berbeda. Oke, jadi, fungsi ini mengembalikan hal-hal ini sebagai Tuple. Mengapa? Dalam proses pengembangan, Anda akan sering mendapati diri Anda perlu mengembalikan struct (jika itu JavaScript, Anda mungkin ingin mengembalikan objek JSON, mungkin). Nah, pada tulisan ini (meskipun di masa depan ini dapat berubah), Solidity tidak mendukung pengembalian struct dari fungsi publik. Jadi, Anda harus mengembalikan tupel sebagai gantinya. Jika Anda seorang pria Python, Anda mungkin sudah nyaman dengan tupel. Banyak bahasa tidak benar-benar mendukung mereka, setidaknya tidak dengan cara ini.
Lihat baris 159 untuk contoh mengembalikan Tuple sebagai nilai pengembalian:
return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);
Dan bagaimana kita menerima nilai kembali dari sesuatu seperti ini? Kita bisa melakukannya seperti:
var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Atau, Anda dapat mendeklarasikan variabel secara eksplisit sebelumnya, dengan tipe yang benar:
//declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
Dan sekarang kita telah mendeklarasikan 7 variabel untuk menampung 7 nilai kembalian, yang sekarang dapat kita gunakan. Jika tidak, seandainya kita hanya menginginkan satu atau dua nilai, kita dapat mengatakan:
//declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);
Lihat apa yang kami lakukan di sana? Kami hanya mendapatkan dua yang kami minati. Lihat semua koma itu. Kita harus menghitungnya dengan cermat!
Impor
Baris 3 dan 4 dari BoxingOracle.sol adalah impor:
import "./Ownable.sol"; import "./DateLib.sol";
Seperti yang mungkin Anda harapkan, ini mengimpor definisi dari file kode yang ada di folder proyek kontrak yang sama dengan BoxingOracle.sol.
pengubah
Perhatikan bahwa definisi fungsi memiliki banyak pengubah yang terpasang. Pertama, ada visibilitas: privat, publik, internal, dan eksternal—visibilitas fungsi.
Selanjutnya, Anda akan melihat kata kunci pure
dan view
. Ini menunjukkan kepada kompiler perubahan seperti apa yang akan dibuat fungsi, jika ada. Hal ini penting karena hal tersebut merupakan faktor biaya akhir gas untuk menjalankan fungsi tersebut. Lihat di sini untuk penjelasannya: Solidity Docs.
Terakhir, yang sebenarnya ingin saya bahas adalah custom modifier. Lihat baris 61 dari BoxingOracle.sol:
function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {
Perhatikan pengubah onlyOwner
tepat sebelum kata kunci "publik". Ini menunjukkan bahwa hanya pemilik kontrak yang dapat memanggil metode ini! Meskipun sangat penting, ini bukan fitur asli Soliditas (walaupun mungkin akan ada di masa mendatang). Sebenarnya onlyOwner
adalah contoh custom modifier yang kita buat sendiri, dan gunakan. Mari kita lihat.
Pertama, pengubah didefinisikan dalam file Ownable.sol, yang dapat Anda lihat telah kami impor pada baris 3 dari BoxingOracle.sol:
import "./Ownable.sol"
Perhatikan bahwa, untuk memanfaatkan pengubah, kami telah membuat BoxingOracle
mewarisi dari Ownable
. Di dalam Ownable.sol, pada baris 25, kita dapat menemukan definisi untuk pengubah di dalam kontrak "Dapat Dimiliki":
modifier onlyOwner() { require(msg.sender == owner); _; }
(Omong-omong, kontrak yang Dapat Dimiliki ini diambil dari salah satu kontrak publik OpenZeppelin.)
Perhatikan bahwa hal ini dideklarasikan sebagai pengubah, yang menunjukkan bahwa kita dapat menggunakannya seperti yang kita miliki, untuk memodifikasi suatu fungsi. Perhatikan bahwa daging dari pengubah adalah pernyataan "memerlukan". Require pernyataan adalah jenis seperti menegaskan, tetapi tidak untuk debugging. Jika kondisi dari pernyataan require gagal, maka fungsi akan mengeluarkan eksepsi. Jadi untuk memparafrasekan pernyataan "membutuhkan" ini:
require(msg.sender == owner);
Kita bisa mengatakan itu berarti:
if (msg.send != owner) throw an exception;
Dan, pada kenyataannya, di Solidity 0.4.22 dan yang lebih tinggi, kita dapat menambahkan pesan kesalahan ke pernyataan yang membutuhkan:
require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");
Akhirnya, di baris yang tampak penasaran:
_;
Garis bawah adalah singkatan untuk "Di sini, jalankan konten lengkap dari fungsi yang dimodifikasi." Jadi pada dasarnya, pernyataan yang dibutuhkan akan dieksekusi terlebih dahulu, diikuti oleh fungsi yang sebenarnya. Jadi ini seperti pra-pending baris logika ini ke fungsi yang dimodifikasi.

Tentu saja ada lebih banyak hal yang dapat Anda lakukan dengan pengubah. Periksa dokumen: Dokumen.
Perpustakaan Soliditas
Ada fitur bahasa Solidity yang dikenal sebagai library . Kami memiliki contoh dalam proyek kami di DateLib.sol.
Ini adalah perpustakaan untuk penanganan tipe tanggal yang lebih mudah. Itu diimpor ke BoxingOracle di baris 4:
import "./DateLib.sol";
Dan itu digunakan pada baris 13:
using DateLib for DateLib.DateTime;
DateLib.DateTime
adalah struct yang dikeluarkan dari kontrak DateLib (terpapar sebagai anggota; lihat baris 4 DateLib.sol) dan kami menyatakan di sini bahwa kami "menggunakan" pustaka DateLib untuk tipe data tertentu. Jadi metode dan operasi yang dideklarasikan di perpustakaan itu akan berlaku untuk tipe data yang kami katakan seharusnya. Begitulah cara perpustakaan digunakan di Solidity.
Untuk contoh yang lebih jelas, periksa beberapa perpustakaan OpenZeppelin untuk angka, seperti SafeMath. Ini dapat diterapkan ke tipe data Soliditas asli (numerik) (sedangkan di sini kami telah menerapkan pustaka ke tipe data kustom), dan digunakan secara luas.
Antarmuka
Seperti dalam bahasa berorientasi objek utama, antarmuka didukung. Antarmuka dalam Soliditas didefinisikan sebagai kontrak, tetapi badan fungsi dihilangkan untuk fungsi. Untuk contoh definisi antarmuka, lihat OracleInterface.sol. Dalam contoh ini, antarmuka digunakan sebagai pengganti untuk kontrak oracle, yang isinya berada dalam kontrak terpisah dengan alamat terpisah.
Konvensi Penamaan
Tentu saja, konvensi penamaan bukanlah aturan global; sebagai programmer, kami tahu bahwa kami bebas untuk mengikuti konvensi pengkodean dan penamaan yang menarik bagi kami. Di sisi lain, kami ingin orang lain merasa nyaman membaca dan bekerja dengan kode kami, sehingga beberapa tingkat standardisasi diinginkan.
Ulasan Proyek
Jadi sekarang kita telah membahas beberapa fitur bahasa umum yang ada dalam file kode yang dimaksud, kita dapat mulai melihat lebih spesifik pada kode itu sendiri, untuk proyek ini.
Jadi, mari kita perjelas tujuan dari proyek ini, sekali lagi. Tujuan dari proyek ini adalah untuk memberikan demonstrasi semi-realistis (atau pseudo-realistis) dan contoh kontrak pintar yang menggunakan oracle. Pada intinya, ini hanyalah sebuah kontrak yang memanggil kontrak lain yang terpisah.
Kasus bisnis dari contoh dapat dinyatakan sebagai berikut:
- Seorang pengguna ingin membuat taruhan dengan berbagai ukuran pada pertandingan tinju, membayar uang (eter) untuk taruhan dan mengumpulkan kemenangan mereka kapan dan jika mereka menang.
- Seorang pengguna membuat taruhan ini melalui kontrak pintar. (Dalam kasus penggunaan kehidupan nyata, ini akan menjadi DApp lengkap dengan front-end web3; tetapi kami hanya memeriksa sisi kontrak.)
- Kontrak cerdas yang terpisah—oracle—dikelola oleh pihak ketiga. Tugasnya adalah menyimpan daftar pertandingan tinju dengan statusnya saat ini (tertunda, sedang berlangsung, selesai, dll.) dan, jika selesai, pemenangnya.
- Kontrak utama mendapatkan daftar pertandingan yang tertunda dari oracle dan menyajikannya kepada pengguna sebagai pertandingan yang “dapat dipertaruhkan”.
- Kontrak utama menerima taruhan hingga dimulainya pertandingan.
- Setelah pertandingan diputuskan, kontrak utama membagi kemenangan dan kekalahan menurut algoritma sederhana, mengambil potongan untuk dirinya sendiri, dan membayar kemenangan berdasarkan permintaan (pecundang hanya kehilangan seluruh taruhan mereka).
Aturan taruhan:
- Ada taruhan minimum yang ditentukan (didefinisikan dalam wei).
- Tidak ada taruhan maksimum; pengguna dapat bertaruh jumlah berapa pun yang mereka suka di atas jumlah minimum.
- Pengguna dapat memasang taruhan hingga pertandingan menjadi "sedang berlangsung".
Algoritma untuk membagi kemenangan:
- Semua taruhan yang diterima ditempatkan ke dalam “pot”.
- Sebagian kecil dikeluarkan dari pot, untuk rumah.
- Setiap pemenang akan diberikan proporsi pot, berbanding lurus dengan ukuran relatif dari taruhan mereka.
- Kemenangan dihitung segera setelah pengguna pertama meminta hasil, setelah pertandingan diputuskan.
- Kemenangan diberikan atas permintaan pengguna.
- Dalam kasus seri, tidak ada yang menang — semua orang mendapatkan kembali taruhannya, dan rumah tidak mengambil potongan.
BoxingOracle: Kontrak Oracle
Fungsi Utama yang Disediakan
Oracle memiliki dua antarmuka, Anda bisa mengatakan: satu disajikan kepada "pemilik" dan pengelola kontrak dan satu disajikan kepada masyarakat umum; yaitu, kontrak yang mengkonsumsi oracle. Pengelolanya, ia menawarkan fungsionalitas untuk memasukkan data ke dalam kontrak, pada dasarnya mengambil data dari dunia luar dan memasukkannya ke dalam blockchain. Untuk publik, ia menawarkan akses hanya baca ke data tersebut. Penting untuk dicatat bahwa kontrak itu sendiri membatasi non-pemilik untuk mengedit data apa pun, tetapi akses baca-saja ke data tersebut diberikan secara publik tanpa batasan.
Untuk pengguna:
- Daftar semua pertandingan
- Daftar pertandingan yang tertunda
- Dapatkan detail pertandingan tertentu
- Dapatkan status dan hasil pertandingan tertentu
Untuk pemilik:
- Masukkan pertandingan
- Ubah status pertandingan
- Tetapkan hasil pertandingan
Cerita pengguna:
- Pertandingan tinju baru diumumkan dan dikonfirmasi pada 9 Mei.
- Saya, pengelola kontrak (mungkin saya adalah jaringan olahraga terkenal atau outlet baru), menambahkan pertandingan yang akan datang ke data oracle di blockchain, dengan status "tertunda." Siapa pun atau kontrak apa pun sekarang dapat menanyakan dan menggunakan data ini sesuka mereka.
- Saat pertandingan dimulai, saya mengatur status pertandingan itu menjadi “sedang berlangsung.”
- Saat pertandingan berakhir, saya mengatur status pertandingan menjadi "selesai" dan mengubah data pertandingan untuk menunjukkan pemenangnya.
Ulasan Kode Oracle
Ulasan ini sepenuhnya didasarkan pada BoxingOracle.sol; nomor baris merujuk file itu.
Pada baris 10 dan 11, kami mendeklarasikan tempat penyimpanan korek api:
Match[] matches; mapping(bytes32 => uint) matchIdToIndex;
matches
hanyalah larik sederhana untuk menyimpan instance kecocokan, dan pemetaan hanyalah fasilitas untuk memetakan ID kecocokan unik (nilai byte32) ke indeksnya dalam larik sehingga jika seseorang memberi kami ID mentah kecocokan, kami dapat gunakan pemetaan ini untuk menemukannya.
Pada baris 17, struktur kecocokan kita didefinisikan dan dijelaskan:
//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 }
Baris 61: Fungsi addMatch
hanya untuk digunakan oleh pemilik kontrak; itu memungkinkan untuk penambahan kecocokan baru ke data yang disimpan.
Baris 80: Fungsi declareOutcome
memungkinkan pemilik kontrak untuk menetapkan kecocokan sebagai “diputuskan”, mengatur peserta yang menang.
Baris 102-166: Semua fungsi berikut dapat dipanggil oleh publik. Ini adalah data read-only yang terbuka untuk umum:
- Fungsi
getPendingMatches
mengembalikan daftar ID dari semua kecocokan yang statusnya saat ini "tertunda." - Fungsi
getAllMatches
mengembalikan daftar ID dari semua kecocokan. - Fungsi
getMatch
mengembalikan detail lengkap dari satu kecocokan, yang ditentukan oleh ID.
Baris 193-204 mendeklarasikan fungsi yang terutama untuk pengujian, debugging, dan diagnostik.
- Fungsi
testConnection
hanya menguji bahwa kita dapat memanggil kontrak. - Fungsi
getAddress
mengembalikan alamat kontrak ini. - Fungsi
addTestData
menambahkan banyak kecocokan uji ke daftar kecocokan.
Jangan ragu untuk menjelajahi kode sedikit sebelum melanjutkan ke langkah berikutnya. Saya sarankan menjalankan kontrak Oracle lagi dalam mode debug (seperti yang dijelaskan di Bagian 1 dari seri ini), panggil fungsi yang berbeda, dan periksa hasilnya.
BoxingBets: Kontrak Klien
Penting untuk menentukan apa yang menjadi tanggung jawab kontrak klien (kontrak taruhan) dan apa yang tidak bertanggung jawab. Kontrak klien tidak bertanggung jawab untuk menyimpan daftar pertandingan tinju yang sebenarnya atau menyatakan hasil mereka. Kami "mempercayai" (ya saya tahu, ada kata sensitif itu—uh oh—kami akan membahas ini di Bagian 3) oracle untuk layanan itu. Kontrak klien bertanggung jawab untuk menerima taruhan. Ini bertanggung jawab atas algoritme yang membagi kemenangan dan mentransfernya ke akun pemenang berdasarkan hasil pertandingan (seperti yang diterima dari oracle).
Selain itu, semuanya berbasis tarikan dan tidak ada peristiwa atau dorongan. Kontrak menarik data dari oracle. Kontrak menarik hasil pertandingan dari oracle (sebagai tanggapan atas permintaan pengguna) dan kontrak menghitung kemenangan dan mentransfernya sebagai tanggapan atas permintaan pengguna.
Fungsi Utama yang Disediakan
- Daftar semua pertandingan yang tertunda
- Dapatkan detail pertandingan tertentu
- Dapatkan status dan hasil pertandingan tertentu
- Pasang taruhan
- Meminta/menerima kemenangan
Tinjauan Kode Klien
Ulasan ini sepenuhnya didasarkan pada BoxingBets.sol; nomor baris merujuk file itu.
Baris 12 dan 13, baris kode pertama dalam kontrak, menentukan beberapa pemetaan di mana kita akan menyimpan data kontrak kita.
Baris 12 memetakan alamat pengguna ke daftar ID. Ini memetakan pengguna ke daftar ID taruhan milik pengguna. Jadi, untuk setiap alamat pengguna yang diberikan, kami dapat dengan cepat mendapatkan daftar semua taruhan yang telah dilakukan oleh pengguna tersebut.
mapping(address => bytes32[]) private userToBets;
Baris 13 memetakan ID unik pertandingan ke daftar contoh taruhan. Dengan ini, kita bisa, untuk setiap pertandingan, mendapatkan daftar semua taruhan yang telah dibuat untuk pertandingan itu.
mapping(bytes32 => Bet[]) private matchToBets;
Baris 17 dan 18 terkait dengan koneksi ke oracle kita. Pertama, dalam variabel boxingOracleAddr
, kami menyimpan alamat kontrak oracle (diatur ke nol secara default). Kami dapat membuat hard-code alamat oracle, tetapi kemudian kami tidak akan pernah bisa mengubahnya. (Tidak dapat mengubah alamat oracle bisa menjadi hal yang baik atau buruk—kita dapat mendiskusikannya di Bagian 3). Baris berikutnya membuat turunan dari antarmuka oracle (yang didefinisikan dalam OracleInterface.sol) dan menyimpannya dalam sebuah variabel.
//boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);
Jika Anda melompat ke baris 58, Anda akan melihat fungsi setOracleAddress
, di mana alamat oracle ini dapat diubah, dan di mana instance boxingOracle
dibuat kembali dengan alamat baru.
Baris 21 mendefinisikan ukuran taruhan minimum kami, di wei. Ini tentu saja sebenarnya jumlah yang sangat kecil, hanya 0,000001 eter.
uint internal minimumBet = 1000000000000;
Pada baris 58 dan 66 masing-masing, kita memiliki fungsi setOracleAddress
dan getOracleAddress
. setOracleAddress
memiliki satu- onlyOwner
pengubahOwner karena hanya pemilik kontrak yang dapat mengganti oracle ke oracle lain (mungkin bukan ide yang baik, tetapi kami akan menguraikannya di Bagian 3). Fungsi getOracleAddress
, di sisi lain, dapat dipanggil secara publik; siapa pun dapat melihat Oracle apa yang digunakan.
function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....
Pada baris 72 dan 79, kita masing-masing memiliki fungsi getBettableMatches
dan getMatch
. Perhatikan bahwa ini hanya meneruskan panggilan ke Oracle, dan mengembalikan hasilnya.
function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....
Fungsi placeBet
adalah fungsi yang sangat penting (baris 108).
function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...
Fitur yang mencolok dari yang satu ini adalah pengubah payable
; kami telah begitu sibuk mendiskusikan fitur bahasa umum sehingga kami belum menyentuh fitur yang sangat penting untuk dapat mengirim uang bersama dengan panggilan fungsi! Itu pada dasarnya apa itu — itu adalah fungsi yang dapat menerima sejumlah uang bersama dengan argumen dan data lain yang dikirim.
Kami membutuhkan ini di sini karena di sinilah pengguna secara bersamaan menentukan taruhan apa yang akan mereka hasilkan, berapa banyak uang yang ingin mereka gunakan untuk taruhan itu, dan benar-benar mengirim uangnya. Pengubah payable
memungkinkan itu. Sebelum menerima taruhan, kami melakukan banyak pemeriksaan untuk memastikan validitas taruhan. Pemeriksaan pertama pada baris 111 adalah:
require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");
Jumlah uang yang dikirim disimpan dalam msg.value
. Dengan asumsi bahwa semua cek lulus, pada baris 123, kami akan mentransfer jumlah itu ke kepemilikan oracle, mengambil kepemilikan jumlah itu dari pengguna, dan ke kepemilikan kontrak:
address(this).transfer(msg.value);
Terakhir, pada baris 136, kita memiliki fungsi pembantu pengujian/debugging yang akan membantu kita mengetahui apakah kontrak terhubung ke oracle yang valid atau tidak:
function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }
Membungkus
Dan ini sebenarnya sejauh contoh ini; hanya menerima taruhan. Fungsionalitas untuk membagi kemenangan dan pembayaran, serta beberapa logika lainnya sengaja ditinggalkan untuk menjaga agar contoh cukup sederhana untuk tujuan kita, yang hanya untuk mendemonstrasikan penggunaan oracle dengan kontrak. Logika yang lebih lengkap dan kompleks itu ada di proyek lain saat ini, yang merupakan perpanjangan dari contoh ini dan masih dalam pengembangan.
Jadi sekarang kita memiliki pemahaman yang lebih baik tentang basis kode, dan telah menggunakannya sebagai sarana dan titik awal untuk membahas beberapa fitur bahasa yang ditawarkan oleh Solidity. Tujuan utama dari seri tiga bagian ini adalah untuk mendemonstrasikan dan mendiskusikan penggunaan kontrak dengan oracle. Tujuan dari bagian ini adalah untuk memahami kode spesifik ini sedikit lebih baik, dan menggunakannya sebagai titik awal untuk memahami beberapa fitur Soliditas dan pengembangan kontrak pintar. Tujuan dari bagian ketiga dan terakhir adalah untuk membahas strategi dan filosofi penggunaan oracle dan bagaimana hal itu cocok secara konseptual ke dalam model kontrak pintar.
Langkah Opsional Lebih Lanjut
Saya akan sangat mendorong pembaca yang ingin mempelajari lebih lanjut, untuk mengambil kode ini dan bermain dengannya. Menerapkan fitur baru. Perbaiki bug apa pun. Menerapkan fitur yang belum diterapkan (seperti antarmuka pembayaran). Uji panggilan fungsi. Ubah dan uji ulang untuk melihat apa yang terjadi. Tambahkan front-end web3. Tambahkan fasilitas untuk menghapus kecocokan atau memodifikasi hasilnya (jika terjadi kesalahan). Bagaimana dengan pertandingan yang dibatalkan? Menerapkan oracle kedua. Tentu saja, sebuah kontrak bebas untuk menggunakan oracle sebanyak yang diinginkannya, tetapi masalah apa yang ditimbulkannya? Bersenang-senanglah dengannya; itu cara yang bagus untuk belajar, dan ketika Anda melakukannya dengan cara itu (dan memperoleh kesenangan darinya), Anda pasti akan mempertahankan lebih banyak dari apa yang telah Anda pelajari.
Contoh daftar hal-hal yang tidak lengkap untuk dicoba:
- Jalankan kontrak dan oracle di testnet lokal (dalam truffle, seperti yang dijelaskan di Bagian 1) dan panggil semua fungsi yang dapat dipanggil dan semua fungsi pengujian.
- Tambahkan fungsionalitas untuk menghitung kemenangan dan membayarnya, setelah menyelesaikan pertandingan.
- Tambahkan fungsionalitas untuk mengembalikan semua taruhan jika terjadi seri.
- Tambahkan fitur untuk meminta pengembalian uang atau pembatalan taruhan, sebelum pertandingan dimulai.
- Tambahkan fitur untuk memungkinkan fakta bahwa pertandingan terkadang bisa dibatalkan (dalam hal ini semua orang akan membutuhkan pengembalian dana).
- Menerapkan fitur untuk menjamin bahwa oracle yang ada saat pengguna memasang taruhan adalah oracle yang sama yang akan digunakan untuk menentukan hasil pertandingan itu.
- Menerapkan oracle (kedua) lain, yang memiliki beberapa fitur berbeda yang terkait dengannya, atau mungkin menyajikan olahraga selain tinju (perhatikan bahwa jumlah dan daftar peserta memungkinkan untuk berbagai jenis olahraga, jadi kami sebenarnya tidak terbatas hanya pada tinju) .
- Terapkan
getMostRecentMatch
sehingga benar-benar mengembalikan kecocokan yang paling baru ditambahkan, atau kecocokan yang paling dekat dengan tanggal saat ini dalam hal kapan itu akan terjadi. - Menerapkan penanganan pengecualian.
Setelah Anda terbiasa dengan mekanisme hubungan antara kontrak dan oracle, di Bagian 3 dari seri tiga bagian ini, kita akan membahas beberapa isu strategis, desain, dan filosofis yang diangkat oleh contoh ini.