部署投票合约并与之交互
Starknet的 Vote
合约首先要通过合约的构造函数注册投票人。在此阶段,三个投票人被初始化,他们的地址被传递给内部函数 _register_voters
。该函数将选民添加到合约的状态中,标记为已注册并有资格投票。
在合约中,常量 YES
和 NO
被定义为表决选项(分别为 1 和 0)。这些常量使输入值标准化,从而方便了投票过程。
注册完成后,投票者可使用 vote
函数进行投票,选择 1(YES)或 0(NO)作为其投票。投票时,合约状态会被更新,记录投票情况并标记投票人已投票。这样可以确保投票者无法在同一提案中再次投票。投票会触发 VoteCast
事件,记录投票行为。
该合约还会监控未经授权的投票尝试。如果检测到未经授权的行为,如非注册用户试图投票或用户试图再次投票,就会发出 UnauthorizedAttempt
事件。
这些功能、状态、常量和事件共同创建了一个结构化投票系统,在Starknet环境中管理着从注册到投票、事件记录、以及结果检索的投票生命周期。常量(如 YES
和 NO
)有助于简化投票流程,而事件则在确保透明度和可追溯性方面发挥着重要作用。
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)
}
}
}
Deploying, Calling and Invoking the Voting Contract
Starknet 体验的一部分就是部署智能合约并与之交互。
一旦部署了合约,我们就可以通过调用合约的函数与之交互:
- Calling contracts: Interacting with external functions that only read from the state. These functions do not alter the state of the network, so they don't require fees or signing.
- Invoking contracts: Interacting with external functions that can write to the state. These functions do alter the state of the network and require fees and signing.
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.
一旦安装了 katana
,就可以用以下命令启动本地Starknet节点:
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
to0.3.6
, use thestarkliup -v 0.3.6
command, or simplystarkliup
which installed the latest stable version.
你可以用以下命令获取智能钱包的 class hash(所有智能钱包的class hash都一样)。注意使用了 --rpc
标志和 katana
提供的 RPC 端点:
starkli class-hash-at <SMART_WALLET_ADDRESS> --rpc http://0.0.0.0:5050
Contract Deployment
在部署之前,我们需要声明合约。我们可以使用 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
如果你使用的编译器版本旧于Starkli使用的版本,并且在使用上述命令时遇到了 compiler-version
错误,你可以通过在命令中添加 --compiler-version x.y.z
标志来指定要使用的编译器版本。
如果你仍然遇到编译器版本的问题,请尝试使用以下命令来升级 Starkli:starkliup
,以确保你正在使用 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.
由于我们使用的是本地节点,因此交易将立即完成。如果您使用的是 Goerli Testnet,则需要等待交易最终完成,这通常需要几秒钟。
以下命令将部署投票合约,并将 voter_0、voter_1 和 voter_2 注册为合格投票人。这些是构造函数参数,因此请添加一个以后可以用来投票的选民账户。
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
命令示例:
starkli deploy 0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 0x033c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c 0x01d98d835e43b032254ffbef0f150c5606fa9c5c9310b1fae370ab956a7919f5 --rpc http://0.0.0.0:5050 --account katana-0
在本例中,合约已部署到特定地址:0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349
。您应该会看到不同的地址。我们将使用此地址与合约进行交互。
Voter Eligibility Verification
在我们的投票合约中,我们有两个函数来验证投票人的资格,即 voter_can_vote
和 is_voter_registered
。这些函数是外部只读函数,这意味着它们不会改变合约的状态,而只是读取当前状态。
is_voter_registered
函数检查特定地址是否在合约中登记为合格投票人。另一方面,voter_can_vote
函数会检查特定地址的投票人当前是否有资格投票,即他们是否已登记且尚未投票。
你可以使用 starkli call
命令来调用这些函数。请注意,call
命令用于只读函数,而 invoke
命令用于也可以写入存储空间的函数。调用 call
命令不需要签名,而 invoke
命令需要签名。
starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 voter_can_vote 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 --rpc http://0.0.0.0:5050
首先,我们添加了合约的地址,然后是要调用的函数,最后是函数的输入。在本例中,我们要检查地址为 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0
的投票人是否可以投票。
由于我们提供了已登记的投票人的地址作为输入,因此结果为 1(布尔值为 true),表明该选民有资格投票。
接下来,让我们使用一个未注册的账户地址调用 is_voter_registered
函数来观察输出结果:
starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 is_voter_registered 0x44444444444444444 --rpc http://0.0.0.0:5050
对于未注册的账户地址,终端输出为 0(即假),确认该账户没有投票资格。
Casting a Vote
既然我们已经确定了如何验证选民资格,那么我们就可以投票了!投票时,我们要与vote
函数交互,该函数被标记为外部函数,因此必须使用 starknet invoke
命令。
invoke
命令的语法与 call
命令类似,但在投票时,我们需要输入 "1"(表示 "是")或 "0"(表示 "否")。当我们唤起 vote
函数时,我们会被收取一定的费用,而且交易必须由投票人签署;我们正在向合约的存储空间写入内容。
//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
系统将提示您输入签名者的密码。输入密码后,交易将被签署并提交到Starknet网络。你将收到交易哈希值作为输出。使用 starkli 交易命令,你可以获得更多关于交易的详细信息:
starkli transaction <TRANSACTION_HASH> --rpc http://0.0.0.0:5050
这个会返回:
{
"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"
]
}
如果您尝试用同一个签名者投票两次,则会出现错误:
Error: code=ContractError, message="Contract error"
错误信息很简略,但你可以启动 katana
(我们的本地Starknet节点)的终端中查看输出,获得更多细节:
...
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\".
...
错误的关键字是 USER_ALREADY_VOTED
。
assert!(can_vote, "USER_ALREADY_VOTED");
我们可以重复上述过程,为要将用于投票的账户创建签名者和账户描述符。请记住,每个签名者都必须用私钥创建,每个账户描述符都必须用公钥、智能钱包地址和智能钱包class hash (每个投票者的class hash 都一样)创建。
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
为了检查投票结果,我们通过 starknet call
命令调用另一个视图函数 get_vote_status
。
starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 get_vote_status --rpc http://0.0.0.0:5050
输出结果显示了 "Yes"和 "No" 票数及其相对百分比。