Mendeploy dan Berinteraksi dengan kontrak pemungutan suara

Kontrak Vote di Starknet dimulai dengan mendaftarkan pemilih melalui konstruktor kontrak. Tiga pemilih diinisialisasi pada tahap ini, dan alamat mereka diteruskan ke fungsi internal _register_voters. Fungsi ini menambahkan pemilih ke status kontrak, menandai mereka sebagai terdaftar dan memenuhi syarat untuk memberikan suara.

Dalam kontrak, konstanta YES dan NO didefinisikan untuk mewakili opsi pemilihan (1 dan 0, masing-masing). Konstan ini memudahkan proses pemilihan dengan standarisasi nilai masukan.

Setelah terdaftar, seorang pemilih dapat memberikan suara menggunakan fungsi vote, memilih 1 (YA) atau 0 (TIDAK) sebagai suaranya. Saat memberikan suara, status kontrak diperbarui, mencatat suara dan menandai pemilih sebagai yang sudah memberikan suara. Hal ini memastikan bahwa pemilih tidak dapat memberikan suara lagi dalam proposal yang sama. Penyelenggaraan suara memicu acara VoteCast, mencatat tindakan tersebut.

Kontrak ini juga memantau upaya pemilihan yang tidak sah. Jika tindakan tidak sah terdeteksi, seperti pengguna yang tidak terdaftar mencoba memberikan suara atau pengguna yang mencoba memberikan suara lagi, acara UnauthorizedAttempt akan dipancarkan.

Secara bersama-sama, fungsi-fungsi, status, konstanta, dan acara-acara ini menciptakan sistem pemilihan terstruktur, mengelola siklus hidup suatu pemilihan mulai dari pendaftaran hingga pencoblosan, pencatatan acara, dan pengambilan hasil dalam lingkungan Starknet. Konstan seperti YES dan NO membantu menyederhanakan proses pemilihan, sementara acara-acara memainkan peran penting dalam memastikan transparansi dan jejak jejak.

Listing 17-7 shows the Vote contract in detail:

/// Core Library Imports for the Traits outside the Starknet Contract
use core::starknet::ContractAddress;

/// Trait defining the functions that can be implemented or called by the Starknet Contract
#[starknet::interface]
trait VoteTrait<T> {
    /// Returns the current vote status
    fn get_vote_status(self: @T) -> (u8, u8, u8, u8);
    /// Checks if the user at the specified address is allowed to vote
    fn voter_can_vote(self: @T, user_address: ContractAddress) -> bool;
    /// Checks if the specified address is registered as a voter
    fn is_voter_registered(self: @T, address: ContractAddress) -> bool;
    /// Allows a user to vote
    fn vote(ref self: T, vote: u8);
}

/// Starknet Contract allowing three registered voters to vote on a proposal
#[starknet::contract]
mod Vote {
    use core::starknet::ContractAddress;
    use core::starknet::get_caller_address;
    use core::starknet::storage::{
        StoragePointerReadAccess, StoragePointerWriteAccess, StorageMapReadAccess,
        StorageMapWriteAccess, Map,
    };

    const YES: u8 = 1_u8;
    const NO: u8 = 0_u8;

    #[storage]
    struct Storage {
        yes_votes: u8,
        no_votes: u8,
        can_vote: Map::<ContractAddress, bool>,
        registered_voter: Map::<ContractAddress, bool>,
    }

    #[constructor]
    fn constructor(
        ref self: ContractState,
        voter_1: ContractAddress,
        voter_2: ContractAddress,
        voter_3: ContractAddress,
    ) {
        self._register_voters(voter_1, voter_2, voter_3);

        self.yes_votes.write(0_u8);
        self.no_votes.write(0_u8);
    }

    #[event]
    #[derive(Drop, starknet::Event)]
    enum Event {
        VoteCast: VoteCast,
        UnauthorizedAttempt: UnauthorizedAttempt,
    }

    #[derive(Drop, starknet::Event)]
    struct VoteCast {
        voter: ContractAddress,
        vote: u8,
    }

    #[derive(Drop, starknet::Event)]
    struct UnauthorizedAttempt {
        unauthorized_address: ContractAddress,
    }

    #[abi(embed_v0)]
    impl VoteImpl of super::VoteTrait<ContractState> {
        fn get_vote_status(self: @ContractState) -> (u8, u8, u8, u8) {
            let (n_yes, n_no) = self._get_voting_result();
            let (yes_percentage, no_percentage) = self._get_voting_result_in_percentage();
            (n_yes, n_no, yes_percentage, no_percentage)
        }

        fn voter_can_vote(self: @ContractState, user_address: ContractAddress) -> bool {
            self.can_vote.read(user_address)
        }

        fn is_voter_registered(self: @ContractState, address: ContractAddress) -> bool {
            self.registered_voter.read(address)
        }

        fn vote(ref self: ContractState, vote: u8) {
            assert!(vote == NO || vote == YES, "VOTE_0_OR_1");
            let caller: ContractAddress = get_caller_address();
            self._assert_allowed(caller);
            self.can_vote.write(caller, false);

            if (vote == NO) {
                self.no_votes.write(self.no_votes.read() + 1_u8);
            }
            if (vote == YES) {
                self.yes_votes.write(self.yes_votes.read() + 1_u8);
            }

            self.emit(VoteCast { voter: caller, vote: vote });
        }
    }

    #[generate_trait]
    impl InternalFunctions of InternalFunctionsTrait {
        fn _register_voters(
            ref self: ContractState,
            voter_1: ContractAddress,
            voter_2: ContractAddress,
            voter_3: ContractAddress,
        ) {
            self.registered_voter.write(voter_1, true);
            self.can_vote.write(voter_1, true);

            self.registered_voter.write(voter_2, true);
            self.can_vote.write(voter_2, true);

            self.registered_voter.write(voter_3, true);
            self.can_vote.write(voter_3, true);
        }
    }

    #[generate_trait]
    impl AssertsImpl of AssertsTrait {
        fn _assert_allowed(ref self: ContractState, address: ContractAddress) {
            let is_voter: bool = self.registered_voter.read((address));
            let can_vote: bool = self.can_vote.read((address));

            if (!can_vote) {
                self.emit(UnauthorizedAttempt { unauthorized_address: address });
            }

            assert!(is_voter, "USER_NOT_REGISTERED");
            assert!(can_vote, "USER_ALREADY_VOTED");
        }
    }

    #[generate_trait]
    impl VoteResultFunctionsImpl of VoteResultFunctionsTrait {
        fn _get_voting_result(self: @ContractState) -> (u8, u8) {
            let n_yes: u8 = self.yes_votes.read();
            let n_no: u8 = self.no_votes.read();

            (n_yes, n_no)
        }

        fn _get_voting_result_in_percentage(self: @ContractState) -> (u8, u8) {
            let n_yes: u8 = self.yes_votes.read();
            let n_no: u8 = self.no_votes.read();

            let total_votes: u8 = n_yes + n_no;

            if (total_votes == 0_u8) {
                return (0, 0);
            }
            let yes_percentage: u8 = (n_yes * 100_u8) / (total_votes);
            let no_percentage: u8 = (n_no * 100_u8) / (total_votes);

            (yes_percentage, no_percentage)
        }
    }
}

Listing 17-7: A voting smart contract

Deploying, Calling and Invoking the Voting Contract

Bagian dari pengalaman Starknet adalah mendeploy dan berinteraksi dengan kontrak pintar.

Setelah kontrak didaftarkan, kita dapat berinteraksi dengan kontrak tersebut dengan melakukan panggilan dan pemanggilan fungsi-fungsinya:

  • Memanggil kontrak: Berinteraksi dengan fungsi eksternal yang hanya membaca dari status. Fungsi-fungsi ini tidak mengubah status jaringan, sehingga tidak memerlukan biaya atau tanda tangan.
  • Memanggil kontrak: Berinteraksi dengan fungsi eksternal yang dapat menulis ke status. Fungsi-fungsi ini mengubah status jaringan dan memerlukan biaya serta tanda tangan.

We will setup a local development node using katana to deploy the voting contract. Then, we'll interact with the contract by calling and invoking its functions. You can also use the Goerli Testnet instead of katana. However, we recommend using katana for local development and testing. You can find the complete tutorial for katana in the "Using a development network" chapter of the Starknet Docs.

The katana Local Starknet Node

katana is designed to support local development by the Dojo team. It will allow you to do everything you need to do with Starknet, but locally. It is a great tool for development and testing.

To install katana from the source code, please refer to the "Using Katana" chapter of the Dojo Engine.

Note: Please verify that the version of katana match the specified version provided below.

$ katana --version
katana 1.0.9-dev (38b3c2a6)

To upgrade katana version, refer to the "Using Katana" chapter of the Dojo Engine.

Setelah Anda menginstal katana, Anda dapat memulai node Starknet lokal dengan:

katana

This command will start a local Starknet node with predeployed accounts. We will use these accounts to deploy and interact with the voting contract:

...
PREFUNDED ACCOUNTS
==================

| Account address |  0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0
| Private key     |  0x0300001800000000300000180000000000030000000000003006001800006600
| Public key      |  0x01b7b37a580d91bc3ad4f9933ed61f3a395e0e51c9dd5553323b8ca3942bb44e

| Account address |  0x033c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c
| Private key     |  0x0333803103001800039980190300d206608b0070db0012135bd1fb5f6282170b
| Public key      |  0x04486e2308ef3513531042acb8ead377b887af16bd4cdd8149812dfef1ba924d

| Account address |  0x01d98d835e43b032254ffbef0f150c5606fa9c5c9310b1fae370ab956a7919f5
| Private key     |  0x07ca856005bee0329def368d34a6711b2d95b09ef9740ebf2c7c7e3b16c1ca9c
| Public key      |  0x07006c42b1cfc8bd45710646a0bb3534b182e83c313c7bc88ecf33b53ba4bcbc
...

Before we can interact with the voting contract, we need to prepare the voter and admin accounts on Starknet. Each voter account must be registered and sufficiently funded for voting. For a more detailed understanding of how accounts operate with Account Abstraction, refer to the "Account Abstraction" chapter of the Starknet Docs.

Smart Wallets for Voting

Aside from Scarb you will need to have Starkli installed. Starkli is a command line tool that allows you to interact with Starknet. You can find the installation instructions in the "Setting up Starkli" chapter of the Starknet Docs.

Note: Please verify that the version of starkli match the specified version provided below.

$ starkli --version
0.3.6 (8d6db8c)

To upgrade starkli to 0.3.6, use the starkliup -v 0.3.6 command, or simply starkliup which installed the latest stable version.

Anda dapat mengambil hash kelas dompet pintar (akan sama untuk semua dompet pintar Anda) dengan perintah berikut. Perhatikan penggunaan flag --rpc dan ujung RPC yang disediakan oleh katana:

starkli class-hash-at <SMART_WALLET_ADDRESS> --rpc http://0.0.0.0:5050

Contract Deployment

Sebelum melakukan penyebaran, kita perlu mendeklarasikan kontrak. Hal ini dapat dilakukan dengan perintah starkli declare:

starkli declare target/dev/listing_99_12_vote_contract_Vote.contract_class.json --rpc http://0.0.0.0:5050 --account katana-0

Jika versi kompilator yang Anda gunakan lebih lama daripada yang digunakan oleh Starkli dan Anda mengalami kesalahan compiler-version saat menggunakan perintah di atas, Anda dapat menentukan versi kompilator yang akan digunakan dalam perintah dengan menambahkan flag --compiler-version x.y.z.

Jika Anda masih mengalami masalah dengan versi kompilator, coba perbarui Starkli menggunakan perintah: starkliup untuk memastikan Anda menggunakan versi terbaru dari starkli.

The class hash of the contract is: 0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52. You can declare this contract on Sepolia testnet and see that the class hash will correspond.

The --rpc flag specifies the RPC endpoint to use (the one provided by katana). The --account flag specifies the account to use for signing the transaction.

Karena kita menggunakan node lokal, transaksi akan mencapai finalitas segera. Jika Anda menggunakan Goerli Testnet, Anda perlu menunggu transaksi menjadi final, yang biasanya memerlukan waktu beberapa detik.

Perintah berikut ini mendeploy kontrak pemungutan suara dan mendaftarkan voter_0, voter_1, dan voter_2 sebagai pemilih yang memenuhi syarat. Ini adalah argumen konstruktor, jadi tambahkan akun pemilih yang nantinya dapat Anda gunakan untuk memberikan suara.

starkli deploy <class_hash_of_the_contract_to_be_deployed> <voter_0_address> <voter_1_address> <voter_2_address> --rpc http://0.0.0.0:5050 --account katana-0

Example command:

starkli deploy 0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 0x033c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c 0x01d98d835e43b032254ffbef0f150c5606fa9c5c9310b1fae370ab956a7919f5 --rpc http://0.0.0.0:5050 --account katana-0

Dalam kasus ini, kontrak telah didisposisikan pada alamat spesifik: 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349. Alamat ini akan berbeda untuk Anda. Kami akan menggunakan alamat ini untuk berinteraksi dengan kontrak.

Voter Eligibility Verification

Dalam kontrak pemungutan suara kami, kami memiliki dua fungsi untuk memvalidasi kelayakan pemilih, voter_can_vote dan is_voter_registered. Ini adalah fungsi baca eksternal, yang berarti mereka tidak mengubah status kontrak tetapi hanya membaca status saat ini.

Fungsi is_voter_registered memeriksa apakah suatu alamat tertentu terdaftar sebagai pemilih yang memenuhi syarat dalam kontrak. Sementara itu, fungsi voter_can_vote memeriksa apakah pemilih pada alamat tertentu saat ini berhak untuk memberikan suara, yaitu, mereka terdaftar dan belum memberikan suara.

Anda dapat memanggil fungsi-fungsi ini menggunakan perintah starkli call. Perhatikan bahwa perintah call digunakan untuk fungsi baca, sementara perintah invoke digunakan untuk fungsi yang juga dapat menulis ke penyimpanan. Perintah call tidak memerlukan penandatanganan, sedangkan perintah invoke memerlukan penandatanganan.

starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 voter_can_vote 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 --rpc http://0.0.0.0:5050

Pertama, kami menambahkan alamat kontrak, kemudian fungsi yang ingin kami panggil, dan akhirnya masukan untuk fungsi tersebut. Dalam hal ini, kami sedang memeriksa apakah pemilih di alamat 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 dapat memberikan suara.

Karena kami memberikan alamat pemilih yang terdaftar sebagai masukan, hasilnya adalah 1 (benar dalam bentuk boolean), menunjukkan bahwa pemilih memenuhi syarat untuk memberikan suara.

Selanjutnya, mari panggil fungsi is_voter_registered menggunakan alamat akun yang tidak terdaftar untuk mengamati keluarannya:

starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 is_voter_registered 0x44444444444444444 --rpc http://0.0.0.0:5050

Dengan alamat akun yang tidak terdaftar, keluaran terminal adalah 0 (yakni, salah), mengkonfirmasi bahwa akun tersebut tidak memenuhi syarat untuk memberikan suara.

Casting a Vote

Sekarang bahwa kita telah menetapkan cara untuk memverifikasi kelayakan pemilih, kita dapat memberikan suara! Untuk memberikan suara, kita berinteraksi dengan fungsi vote, yang ditandai sebagai eksternal, memerlukan penggunaan perintah starknet invoke.

Sintaks perintah invoke mirip dengan perintah call, tetapi untuk memberikan suara, kita mengirimkan entah 1 (untuk Ya) atau 0 (untuk Tidak) sebagai masukan kita. Saat kita memanggil fungsi vote, kita dikenakan biaya, dan transaksi harus ditandatangani oleh pemilih; kita sedang menulis ke penyimpanan kontrak.

//Voting Yes
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 1 --rpc http://0.0.0.0:5050 --account katana-0

//Voting No
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 0 --rpc http://0.0.0.0:5050 --account katana-0

Anda akan diminta untuk memasukkan kata sandi untuk penandatangan. Setelah Anda memasukkan kata sandi, transaksi akan ditandatangani dan dikirimkan ke jaringan Starknet. Anda akan menerima hash transaksi sebagai keluaran. Dengan perintah starkli transaction, Anda dapat mendapatkan lebih banyak rincian tentang transaksi:

starkli transaction <TRANSACTION_HASH> --rpc http://0.0.0.0:5050

This returns:

{
  "transaction_hash": "0x5604a97922b6811060e70ed0b40959ea9e20c726220b526ec690de8923907fd",
  "max_fee": "0x430e81",
  "version": "0x1",
  "signature": [
    "0x75e5e4880d7a8301b35ff4a1ed1e3d72fffefa64bb6c306c314496e6e402d57",
    "0xbb6c459b395a535dcd00d8ab13d7ed71273da4a8e9c1f4afe9b9f4254a6f51"
  ],
  "nonce": "0x3",
  "type": "INVOKE",
  "sender_address": "0x3ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0",
  "calldata": [
    "0x1",
    "0x5ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349",
    "0x132bdf85fc8aa10ac3c22f02317f8f53d4b4f52235ed1eabb3a4cbbe08b5c41",
    "0x0",
    "0x1",
    "0x1",
    "0x1"
  ]
}

Jika Anda mencoba memberikan suara dua kali dengan penandatangan yang sama, Anda akan mendapatkan kesalahan:

Error: code=ContractError, message="Contract error"

Error ini tidak memberikan informasi yang sangat informatif, tetapi Anda dapat mendapatkan lebih banyak detail saat melihat output di terminal di mana Anda memulai katana (node Starknet lokal kita):

...
Transaction execution error: "Error in the called contract (0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0):
    Error at pc=0:81:
    Got an exception while executing a hint: Custom Hint Error: Execution failed. Failure reason: \"USER_ALREADY_VOTED\".
    ...

The key for the error is USER_ALREADY_VOTED.

assert!(can_vote, "USER_ALREADY_VOTED");

Kita dapat mengulangi proses ini untuk membuat Signer dan Account Descriptor untuk akun yang ingin kita gunakan untuk memberikan suara. Ingatlah bahwa setiap Signer harus dibuat dari kunci pribadi, dan setiap Account Descriptor harus dibuat dari kunci publik, alamat smart wallet, dan hash kelas smart wallet (yang sama untuk setiap pemilih).

starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 0 --rpc http://0.0.0.0:5050 --account katana-0

starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 1 --rpc http://0.0.0.0:5050 --account katana-0

Visualizing Vote Outcomes

Untuk meneliti hasil pemungutan suara, kita memanggil fungsi get_vote_status, fungsi tampilan lain, melalui perintah starknet call.

starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 get_vote_status --rpc http://0.0.0.0:5050

Output menunjukkan jumlah suara "Ya" dan "Tidak" beserta persentasenya.