部署表决合约并与之互动
Starknet的 Vote
合约首先要通过合约的构造函数注册投票人。在此阶段,三个投票人被初始化,他们的地址被传递给内部函数 _register_voters
。该函数将选民添加到合约的状态中,标记为已注册并有资格投票。
在合约中,常量 YES
和 NO
被定义为表决选项(分别为 1 和 0)。这些常量使输入值标准化,从而方便了投票过程。
注册完成后,投票者可使用 vote
函数进行投票,选择 1(YES)或 0(NO)作为其投票。投票时,合约状态会被更新,记录投票情况并标记投票人已投票。这样可以确保投票者无法在同一提案中再次投票。投票会触发 VoteCast
事件,记录投票行为。
该合约还会监控未经授权的投票尝试。如果检测到未经授权的行为,如非注册用户试图投票或用户试图再次投票,就会发出 UnauthorizedAttempt
事件。
这些功能、状态、常量和事件共同创建了一个结构化投票系统,在Starknet环境中管理着从注册到投票、事件记录、以及结果检索的投票生命周期。常量(如 YES
和 NO
)有助于简化投票流程,而事件则在确保透明度和可追溯性方面发挥着重要作用。
/// @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
#[external(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)
}
}
}
部署、调用和唤起投票合约
Starknet 体验的一部分就是部署智能合约并与之交互。
一旦部署了合约,我们就可以通过调用合约的函数与之交互:
- 调用合约:与只读取状态的外部函数交互。这些函数不会改变网络的状态,因此不需要付费或签署。
- 唤起合约:与可以写入状态的外部函数交互。这些函数会改变网络状态,因此需要付费或签署。
我们将使用 katana
设置一个本地开发节点来部署投票合约。然后,我们将通过调用和调用其函数与合约进行交互。你也可以使用Goerli测试网络而不是 katana
。然而,我们建议在本地开发和测试中使用 katana
。你可以在Starknet Book的 Local Development with Katana 章节中找到有关 katana
的完整教程。
katana
本地Starknet节点
katana
旨在支持Dojo 团队的本地开发。通过它,您可以在本地完成Starknet所需的一切工作。它是开发和测试的绝佳工具。
要从源代码安装 katana
,请参考Starknet Book的 Local Development with Katana 章节。
一旦安装了 katana
,就可以用以下命令启动本地Starknet节点:
katana --accounts 3 --seed 0 --gas-price 250
该命令将启动一个本地 Starknet 节点,并部署 3 个账户。我们将使用这些账户部署投票合约并与之交互:
...
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
...
在我们与投票合约进行交互之前,我们需要在Starknet上准备选民和管理员账户。每个选民账户必须进行注册,并具备足够的资金进行投票。如果你想更详细地了解账户如何与账户抽象一起操作,请参考Starknet Book的 账户抽象 章节。
用于投票的智能钱包
除了Scarb之外,你还需要安装Starkli。Starkli是一个命令行工具,允许你与Starknet进行交互。你可以在Starknet Book的 环境初始化 章节中找到安装说明。
对于我们将使用的每个智能钱包,我们必须在加密的密钥库中创建一个签名者和一个账户描述符。这个过程也在Starknet Book的环境初始化 章节中详细介绍了。
我们可以为要用于投票的账户创建签名者和账户描述符。让我们在智能合约中创建一个用于投票的智能钱包。
首先,我们用私钥创建一个签名者:
starkli signer keystore from-key ~/.starkli-wallets/deployer/account0_keystore.json
然后,我们通过获取我们要使用的katana账户来创建账户描述符:
starkli account fetch <KATANA ACCOUNT ADDRESS> --rpc http://0.0.0.0:5050 --output ~/.starkli-wallets/deployer/account0_account.json
这个命令将创建一个新的 account0_account.json
文件,其中包含以下细节:
{
"version": 1,
"variant": {
"type": "open_zeppelin",
"version": 1,
"public_key": "<SMART_WALLET_PUBLIC_KEY>"
},
"deployment": {
"status": "deployed",
"class_hash": "<SMART_WALLET_CLASS_HASH>",
"address": "<SMART_WALLET_ADDRESS>"
}
}
你可以用以下命令获取智能钱包的 class hash(所有智能钱包的class hash都一样)。注意使用了 --rpc
标志和 katana
提供的 RPC 端点:
starkli class-hash-at <SMART_WALLET_ADDRESS> --rpc http://0.0.0.0:5050
要获取公钥,可以使用 starkli signer keystore inspect
命令,并输入 keystore json 文件的目录:
starkli signer keystore inspect ~/.starkli-wallets/deployer/account0_keystore.json
如果您想拥有第二个和第三个投票人,用同样的过程对 account_1
和 account_2
进行操作即可。
合约部署
在部署之前,我们需要声明合约。我们可以使用 starkli declare
命令来完成这项工作:
starkli declare target/dev/starknetbook_chapter_2_Vote.sierra.json --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
如果你使用的编译器版本旧于Starkli使用的版本,并且在使用上述命令时遇到了 compiler-version
错误,你可以通过在命令中添加 --compiler-version x.y.z
标志来指定要使用的编译器版本。
如果你仍然遇到编译器版本的问题,请尝试使用以下命令来升级 Starkli:starkliup
,以确保你正在使用 starkli 的最新版本。
合约的的class hash是0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52
.您可以在 [任何区块浏览器] (https://goerli.voyager.online/class/0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52)中看到他。
--rpc
标志指定要使用的 RPC 端点(由 katana
提供)。--account
标志指定用于签署交易的账户。这里使用的账户是上一步创建的账户。 --keystore
标记用于指定签署交易的密钥存储文件。
由于我们使用的是本地节点,因此交易将立即完成。如果您使用的是 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 ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
命令示例:
starkli deploy 0x06974677a079b7edfadcd70aa4d12aac0263a4cda379009fca125e0ab1a9ba52 0x03ee9e18edc71a6df30ac3aca2e0b02a198fbce19b7480a63a0d71cbd76652e0 0x033c627a3e5213790e246a917770ce23d7e562baa5b4d2917c23b1be6d91961c 0x01d98d835e43b032254ffbef0f150c5606fa9c5c9310b1fae370ab956a7919f5 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
在本例中,合约已部署到特定地址:0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349
。您应该会看到不同的地址。我们将使用此地址与合约进行交互。
投票人资格验证
在我们的投票合约中,我们有两个函数来验证投票人的资格,即 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(即假),确认该账户没有投票资格。
投票
既然我们已经确定了如何验证选民资格,那么我们就可以投票了!投票时,我们要与vote
函数交互,该函数被标记为外部函数,因此必须使用 starknet invoke
命令。
invoke
命令的语法与 call
命令类似,但在投票时,我们需要输入 "1"(表示 "是")或 "0"(表示 "否")。当我们唤起 vote
函数时,我们会被收取一定的费用,而且交易必须由投票人签署;我们正在向合约的存储空间写入内容。
//Voting Yes
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 1 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
//Voting No
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 0 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account0_account.json --keystore ~/.starkli-wallets/deployer/account0_keystore.json
系统将提示您输入签名者的密码。输入密码后,交易将被签署并提交到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 == true, 'USER_ALREADY_VOTED');
我们可以重复上述过程,为要将用于投票的账户创建签名者和账户描述符。请记住,每个签名者都必须用私钥创建,每个账户描述符都必须用公钥、智能钱包地址和智能钱包class hash (每个投票者的class hash 都一样)创建。
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 0 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account1_account.json --keystore ~/.starkli-wallets/deployer/account1_keystore.json
starkli invoke 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 vote 1 --rpc http://0.0.0.0:5050 --account ~/.starkli-wallets/deployer/account2_account.json --keystore ~/.starkli-wallets/deployer/account2_keystore.json
投票结果可视化
为了检查投票结果,我们通过 starknet call
命令调用另一个视图函数 get_vote_status
。
starkli call 0x05ea3a690be71c7fcd83945517f82e8861a97d42fca8ec9a2c46831d11f33349 get_vote_status --rpc http://0.0.0.0:5050
输出结果显示了 "Yes"和 "No" 票数及其相对百分比。