Install starkli
Open a new terminal
curl https://get.starkli.sh | sh
starkliup
You can check your installation by running starkli --version
,then you will get the starkli version.
Install katana
curl -L https://install.dojoengine.org | bash
dojoup -v 0.6.0-alpha.3
You can check your installation by running katana --version
,then you will get the katana version.
Basics of Katana and Starkli
katana sequencer
katana --disable-fee
After starting the node, a list of accounts will be automatically generated and deployed.
Starkli built-in accounts and configuration
Starkli supports a list of built-in accounts for katana. These built-in accounts are for local development and no one should be using them for anything serious anyways. For example the address of the Katana-0
built-in account is 0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03
. You can check the full list of account addresses here.
To facilitate the seamless execution of Starkli commands, it is crucial to configure specific environment variables. Two primary variables are essential for this purpose:
Account Configuration:
Create an environment variable for your account, specifically for a pre-funded account on the local development network.
export STARKNET_ACCOUNT=katana-0
Network Designation:
Establish another environment variable to specify the network, particularly targeting the local katana devnet. bash
export STARKNET_RPC=http://0.0.0.0:5050
Contract Deployment and Interaction
- Create a Vote project
scarb new vote
Add contract dependencies to scarb.toml
[dependencies]
starknet = "2.5.0"
[[target.starknet-contract]]
Copy the vote contract to lib.cairo
/// @dev Core Library Imports for the Traits outside the Starknet Contract
use starknet::ContractAddress;
/// @dev Trait defining the functions that can be implemented or called by the Starknet Contract
#[starknet::interface]
trait VoteTrait<T> {
/// @dev Function that returns the current vote status
fn get_vote_status(self: @T) -> (u8, u8, u8, u8);
/// @dev Function that checks if the user at the specified address is allowed to vote
fn voter_can_vote(self: @T, user_address: ContractAddress) -> bool;
/// @dev Function that checks if the specified address is registered as a voter
fn is_voter_registered(self: @T, address: ContractAddress) -> bool;
/// @dev Function that allows a user to vote
fn vote(ref self: T, vote: u8);
}
/// @dev Starknet Contract allowing three registered voters to vote on a proposal
#[starknet::contract]
mod Vote {
use starknet::ContractAddress;
use starknet::get_caller_address;
const YES: u8 = 1_u8;
const NO: u8 = 0_u8;
/// @dev Structure that stores vote counts and voter states
#[storage]
struct Storage {
yes_votes: u8,
no_votes: u8,
can_vote: LegacyMap::<ContractAddress, bool>,
registered_voter: LegacyMap::<ContractAddress, bool>,
}
/// @dev Contract constructor initializing the contract with a list of registered voters and 0 vote count
#[constructor]
fn constructor(
ref self: ContractState,
voter_1: ContractAddress,
voter_2: ContractAddress,
voter_3: ContractAddress
) {
// Register all voters by calling the _register_voters function
self._register_voters(voter_1, voter_2, voter_3);
// Initialize the vote count to 0
self.yes_votes.write(0_u8);
self.no_votes.write(0_u8);
}
/// @dev Event that gets emitted when a vote is cast
#[event]
#[derive(Drop, starknet::Event)]
enum Event {
VoteCast: VoteCast,
UnauthorizedAttempt: UnauthorizedAttempt,
}
/// @dev Represents a vote that was cast
#[derive(Drop, starknet::Event)]
struct VoteCast {
voter: ContractAddress,
vote: u8,
}
/// @dev Represents an unauthorized attempt to vote
#[derive(Drop, starknet::Event)]
struct UnauthorizedAttempt {
unauthorized_address: ContractAddress,
}
/// @dev Implementation of VoteTrait for ContractState
#[abi(embed_v0)]
impl VoteImpl of super::VoteTrait<ContractState> {
/// @dev Returns the voting results
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)
}
/// @dev Check whether a voter is allowed to vote
fn voter_can_vote(self: @ContractState, user_address: ContractAddress) -> bool {
self.can_vote.read(user_address)
}
/// @dev Check whether an address is registered as a voter
fn is_voter_registered(self: @ContractState, address: ContractAddress) -> bool {
self.registered_voter.read(address)
}
/// @dev Submit a vote
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, });
}
}
/// @dev Internal Functions implementation for the Vote contract
#[generate_trait]
impl InternalFunctions of InternalFunctionsTrait {
/// @dev Registers the voters and initializes their voting status to true (can vote)
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);
}
}
/// @dev Asserts implementation for the Vote contract
#[generate_trait]
impl AssertsImpl of AssertsTrait {
// @dev Internal function that checks if an address is allowed to vote
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 == false) {
self.emit(UnauthorizedAttempt { unauthorized_address: address, });
}
assert(is_voter == true, 'USER_NOT_REGISTERED');
assert(can_vote == true, 'USER_ALREADY_VOTED');
}
}
/// @dev Implement the VotingResultTrait for the Vote contract
#[generate_trait]
impl VoteResultFunctionsImpl of VoteResultFunctionsTrait {
// @dev Internal function to get the voting results (yes and no vote counts)
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)
}
// @dev Internal function to calculate the voting results in percentage
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)
}
}
}
- Compile contract
scarb build
-
Environment variables
Having compiled the smart contract, it's time to declare it with Starkli and katana. For convenient management, place the following environment variables in a .env file within the
src/
directory.
export STARKNET_ACCOUNT=katana-0
export STARKNET_RPC=http://0.0.0.0:5050
Then, ensure your project acknowledges the environment variables:
source .env
These settings significantly streamline Starkli command operations, ensuring a smoother and more efficient workflow.
- Declare contract
Make sure Katana is already running in separate terminal. Otherwise launch katana
katana --disable-fee
To declare your contract, execute:
starkli declare target/dev/vote_Vote.contract_class.json
Upon successful command execution, you'll obtain a contract class hash: This unique hash serves as the identifier for your contract class within Starknet. For example:
Class hash declared: 0x071092406ababbba5573bbff0074b068aaeb48c9a67ec66abe982ab19bc6997b
- Deploy contract
starkli deploy <class_hash_of_the_contract_to_be_deployed> <voter_0_address> <voter_1_address> <voter_2_address>
There are four hexadecimal numbers in total. The first one is the class_hash of the contract, and the next three are the vote account addresses. We can define that the first vote account address corresponds to the address of katana-0
, while the second and third vote account addresses are associated with katana-1
and katana-2
, respectively. Check the list of built-in accounts here.
starkli deploy 0x071092406ababbba5573bbff0074b068aaeb48c9a67ec66abe982ab19bc6997b 0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03 0x2d71e9c974539bb3ffb4b115e66a23d0f62a641ea66c4016e903454c8753bbc 0x6b86e40118f29ebe393a75469b4d926c7a44c2e2681b6d319520b7c1156d114
After running, expect an output similar to:
Deploying class 0x071092406ababbba5573bbff0074b068aaeb48c9a67ec66abe982ab19bc6997b with salt 0x04baae9a396c3ce27a45b201528ec13b366c25960d640a9a32a8736814d9d8c2...
The contract will be deployed at address 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15
Contract deployment transaction: 0x071d6b51e52febfaf2e3ed7dbbf1416190f019d80c73b1bf707d375374ab7cc5
Contract deployed:
0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15
- Call contract [only read state]
The first parameter is the contract address, the second parameter is the function to be called, and the third parameter is the function parameter. Let's pass the address of Katana-0
account
starkli call 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 voter_can_vote 0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03
After running, expect an output similar to:
[
"0x0000000000000000000000000000000000000000000000000000000000000001"
]
1
means this user address can vote.
- Invoke contract [can write state]
The first parameter is the contract address, the second parameter is the function to be invoked, and the third parameter is the function parameter. Let's vote Yes
with katana-0
user
starkli invoke 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 vote 1
Now let's vote No
with katana-1
user
starkli invoke 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 vote 0 --account katana-1
Let's try to vote again with katana-0
user
starkli invoke 0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15 vote 0
Since the same user/signer cannot vote repeatedly Katana will report an error.
Transaction execution error: "Error in the called contract (0x06162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03):
Error at pc=0:4573:
Got an exception while executing a hint: Hint Error: Execution failed. Failure reason: 0x555345525f414c52454144595f564f544544 ('USER_ALREADY_VOTED').
Cairo traceback (most recent call last):
Unknown location (pc=0:67)
Unknown location (pc=0:1835)
Unknown location (pc=0:2478)
Unknown location (pc=0:3255)
Unknown location (pc=0:3795)
Error in the called contract (0x02c44f2d396fc5f9caa551e8c1d901d943a3b8cc5c433c88a1bf10b1f15fcd15):
Execution failed. Failure reason: 0x555345525f414c52454144595f564f544544 ('USER_ALREADY_VOTED').
- Query transaction
### starkli transaction <TRANSACTION_HASH>
starkli transaction 0x071d6b51e52febfaf2e3ed7dbbf1416190f019d80c73b1bf707d375374ab7cc5
All the above interaction processes can be seen on the katana client. Pay attention to the status changes of katana at each step.