From 3e73c58f5432e03cc9b347650ebd541467a8d80f Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:14:27 +0800 Subject: [PATCH 01/23] feat: update voting rules --- src/events.rs | 1 + src/lib.rs | 52 +++++++++++++++++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 14 deletions(-) diff --git a/src/events.rs b/src/events.rs index 4784a16..8464082 100644 --- a/src/events.rs +++ b/src/events.rs @@ -24,6 +24,7 @@ pub enum Event<'a> { proposal: &'a String, approval_timestamp_ms: &'a U64, deadline_timestamp_ms: &'a U64, + yes_stake: &'a U128, voted_stake: &'a U128, total_stake: &'a U128, num_votes: &'a U64, diff --git a/src/lib.rs b/src/lib.rs index 349d558..35b8010 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,6 +22,12 @@ pub enum Vote { No, } +#[near(serializers = [borsh, json])] +pub struct VotedStake { + vote: Vote, + stake: Balance, +} + const GET_OWNER_ID_GAS: Gas = Gas::from_tgas(5); #[ext_contract(ext_staking_pool)] @@ -36,8 +42,9 @@ pub trait StakingPoolContract { pub struct Contract { proposal: String, deadline_timestamp_ms: Timestamp, - votes: HashMap, - total_voted_stake: Balance, + votes: HashMap, + yes_stake: Balance, // YES voted stake + total_voted_stake: Balance, // YES + NO result: Option, last_epoch_height: EpochHeight, } @@ -57,6 +64,7 @@ impl Contract { proposal, deadline_timestamp_ms, votes: HashMap::new(), + yes_stake: 0, total_voted_stake: 0, result: None, last_epoch_height: 0, @@ -65,18 +73,21 @@ impl Contract { /// Ping to update the votes according to current stake of validators. pub fn ping(&mut self) { - require!( - env::block_timestamp_ms() < self.deadline_timestamp_ms, - "Voting deadline has already passed" - ); require!(self.result.is_none(), "Voting has already ended"); let cur_epoch_height = env::epoch_height(); if cur_epoch_height != self.last_epoch_height { self.total_voted_stake = 0; - for (account_id, stake) in self.votes.iter_mut() { + self.yes_stake = 0; + for (account_id, voted_stake) in self.votes.iter_mut() { let account_current_stake = validator_stake(account_id); self.total_voted_stake += account_current_stake; - *stake = account_current_stake; + if voted_stake.vote == Vote::Yes { + self.yes_stake += account_current_stake; + } + *voted_stake = VotedStake { + vote: voted_stake.vote, + stake: account_current_stake, + }; } self.check_result(); self.last_epoch_height = cur_epoch_height; @@ -128,17 +139,26 @@ impl Contract { Vote::No => 0, }; - let voted_stake = self.votes.remove(&account_id).unwrap_or_default(); + let voted_stake = self.votes.get(&account_id).unwrap_or_default(); require!( - voted_stake <= self.total_voted_stake, + voted_stake.stake <= self.total_voted_stake, format!( "invariant: voted stake {} is more than total voted stake {}", - voted_stake, self.total_voted_stake + voted_stake.stake, self.total_voted_stake ) ); - self.total_voted_stake = self.total_voted_stake + account_stake - voted_stake; + self.total_voted_stake = self.total_voted_stake + account_stake - voted_stake.stake; + if vote == Vote::Yes { + self.yes_stake = self.yes_stake + account_stake - voted_stake.stake; + } if account_stake > 0 { - self.votes.insert(account_id.clone(), account_stake); + self.votes.insert( + account_id.clone(), + VotedStake { + vote, + stake: account_stake, + }, + ); self.check_result(); } // emit event @@ -155,13 +175,17 @@ impl Contract { self.result.is_none(), "check result is called after result is already set" ); + if env::block_timestamp_ms() < self.deadline_timestamp_ms { + return; + } let total_stake = validator_total_stake(); - if self.total_voted_stake > total_stake * 2 / 3 { + if self.total_voted_stake > total_stake / 3 && self.yes_stake > total_voted_stake * 2 / 3 { self.result = Some(env::block_timestamp_ms()); Event::ProposalApproved { proposal: &self.proposal, approval_timestamp_ms: &U64::from(env::block_timestamp_ms()), deadline_timestamp_ms: &U64::from(self.deadline_timestamp_ms), + yes_stake: &U128::from(self.yes_stake), voted_stake: &U128::from(self.total_voted_stake), total_stake: &U128::from(total_stake), num_votes: &U64::from(self.votes.len() as u64), From 1b44776df4f463f29a3ddff10f3b059b0cf3df3a Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 03:39:46 +0000 Subject: [PATCH 02/23] fix: build failure --- src/lib.rs | 68 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 26 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 35b8010..6ab3bf0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,7 +15,8 @@ type Balance = u128; /// Timestamp in milliseconds type Timestamp = u64; -#[near(serializers = [json])] +#[derive(PartialEq, Clone, Debug)] +#[near(serializers = [borsh, json])] #[serde(rename_all = "lowercase")] pub enum Vote { Yes, @@ -85,7 +86,7 @@ impl Contract { self.yes_stake += account_current_stake; } *voted_stake = VotedStake { - vote: voted_stake.vote, + vote: voted_stake.vote.clone(), stake: account_current_stake, }; } @@ -139,29 +140,31 @@ impl Contract { Vote::No => 0, }; - let voted_stake = self.votes.get(&account_id).unwrap_or_default(); + let prev_stake = if let Some(voted_stake) = self.votes.get(&account_id) { + voted_stake.stake + } else { + 0 + }; require!( - voted_stake.stake <= self.total_voted_stake, + prev_stake <= self.total_voted_stake, format!( "invariant: voted stake {} is more than total voted stake {}", - voted_stake.stake, self.total_voted_stake + prev_stake, self.total_voted_stake ) ); - self.total_voted_stake = self.total_voted_stake + account_stake - voted_stake.stake; + self.total_voted_stake = self.total_voted_stake + account_stake - prev_stake; if vote == Vote::Yes { - self.yes_stake = self.yes_stake + account_stake - voted_stake.stake; - } - if account_stake > 0 { - self.votes.insert( - account_id.clone(), - VotedStake { - vote, - stake: account_stake, - }, - ); - self.check_result(); + self.yes_stake = self.yes_stake + account_stake - prev_stake; } - // emit event + self.votes.insert( + account_id.clone(), + VotedStake { + vote: vote.clone(), + stake: account_stake, + }, + ); + self.check_result(); + Event::Voted { validator_id: &account_id, vote: &vote, @@ -179,7 +182,9 @@ impl Contract { return; } let total_stake = validator_total_stake(); - if self.total_voted_stake > total_stake / 3 && self.yes_stake > total_voted_stake * 2 / 3 { + if self.total_voted_stake > total_stake / 3 + && self.yes_stake > self.total_voted_stake * 2 / 3 + { self.result = Some(env::block_timestamp_ms()); Event::ProposalApproved { proposal: &self.proposal, @@ -198,11 +203,12 @@ impl Contract { /// View methods #[near] impl Contract { - /// Returns a pair of `total_voted_stake` and the total stake. + /// Returns a triple of (`yes_stake`, `total_voted_stake`, the total stake). /// Note: as a view method, it doesn't recompute the active stake. May need to call `ping` to /// update the active stake. - pub fn get_total_voted_stake(&self) -> (U128, U128) { + pub fn get_total_voted_stake(&self) -> (U128, U128, U128) { ( + self.yes_stake.into(), self.total_voted_stake.into(), validator_total_stake().into(), ) @@ -211,10 +217,15 @@ impl Contract { /// Returns all active votes. /// Note: as a view method, it doesn't recompute the active stake. May need to call `ping` to /// update the active stake. - pub fn get_votes(&self) -> HashMap { + pub fn get_votes(&self) -> HashMap { self.votes .iter() - .map(|(account_id, stake)| (account_id.clone(), (*stake).into())) + .map(|(account_id, voted_stake)| { + ( + account_id.clone(), + (voted_stake.vote.clone(), voted_stake.stake.into()), + ) + }) .collect() } @@ -415,11 +426,16 @@ mod tests { set_context(&context); assert_eq!( contract.get_total_voted_stake(), - (U128::from(10 * (i + 1) as u128), U128::from(3000)) + ( + U128::from(10 * (i + 1) as u128), + U128::from(10 * (i + 1) as u128), + U128::from(3000) + ) ); // check votes - let expected_votes: HashMap = - (0..=i).map(|j| (validator(j), U128::from(10))).collect(); + let expected_votes: HashMap = (0..=i) + .map(|j| (validator(j), (Vote::Yes, U128::from(10)))) + .collect(); assert_eq!(contract.get_votes(), expected_votes); assert_eq!(contract.get_votes().len() as u64, i + 1); // check voting result From b893b47759a6d5d639c6429eeb4ff3db1fc40773 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 03:53:01 +0000 Subject: [PATCH 03/23] fix: doesn't allow vote after deadline --- src/lib.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 6ab3bf0..9cee1c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -98,6 +98,11 @@ impl Contract { /// Method for validators to vote with `Yes` or `No`. /// The method is called by validator owners. pub fn vote(&mut self, vote: Vote, staking_pool_id: AccountId) -> Promise { + require!( + env::block_timestamp_ms() < self.deadline_timestamp_ms, + "Voting deadline has already passed" + ); + ext_staking_pool::ext(staking_pool_id.clone()) .with_static_gas(GET_OWNER_ID_GAS) .get_owner_id() @@ -505,7 +510,7 @@ mod tests { let context = get_context_with_epoch_height(&voting_contract_id(), 2); set_context_and_validators(&context, &validators); vote(&mut contract, Vote::No, &validator(1)); - assert!(contract.get_votes().is_empty()); + assert_eq!(contract.get_votes().len(), 1); // vote YES at epoch 3 let context = get_context_with_epoch_height(&voting_contract_id(), 3); set_context_and_validators(&context, &validators); @@ -583,7 +588,6 @@ mod tests { } #[test] - #[should_panic(expected = "Voting deadline has already passed")] fn test_ping_after_deadline() { let mut contract = get_contract(); let mut context = get_context(&voting_contract_id()); @@ -599,5 +603,8 @@ mod tests { .epoch_height(2), ); contract.ping(); + + // has vote result + assert!(contract.get_result().is_some()); } } From f1840943adf496efcb65e34a161f6129828cd0e8 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 06:40:54 +0000 Subject: [PATCH 04/23] fix: unit test failure --- src/events.rs | 9 +++++++ src/lib.rs | 68 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 51 insertions(+), 26 deletions(-) diff --git a/src/events.rs b/src/events.rs index 8464082..74051c3 100644 --- a/src/events.rs +++ b/src/events.rs @@ -29,6 +29,15 @@ pub enum Event<'a> { total_stake: &'a U128, num_votes: &'a U64, }, + ProposalRejected { + proposal: &'a String, + rejection_timestamp_ms: &'a U64, + deadline_timestamp_ms: &'a U64, + yes_stake: &'a U128, + voted_stake: &'a U128, + total_stake: &'a U128, + num_votes: &'a U64, + }, } impl Event<'_> { diff --git a/src/lib.rs b/src/lib.rs index 9cee1c0..8b6a8d7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,7 +46,7 @@ pub struct Contract { votes: HashMap, yes_stake: Balance, // YES voted stake total_voted_stake: Balance, // YES + NO - result: Option, + result: Option, last_epoch_height: EpochHeight, } @@ -98,11 +98,6 @@ impl Contract { /// Method for validators to vote with `Yes` or `No`. /// The method is called by validator owners. pub fn vote(&mut self, vote: Vote, staking_pool_id: AccountId) -> Promise { - require!( - env::block_timestamp_ms() < self.deadline_timestamp_ms, - "Voting deadline has already passed" - ); - ext_staking_pool::ext(staking_pool_id.clone()) .with_static_gas(GET_OWNER_ID_GAS) .get_owner_id() @@ -135,6 +130,11 @@ impl Contract { /// Internal method for voting. fn internal_vote(&mut self, vote: Vote, account_id: AccountId) { + require!( + env::block_timestamp_ms() < self.deadline_timestamp_ms, + "Voting deadline has already passed" + ); + self.ping(); let stake = validator_stake(&account_id); @@ -190,7 +190,7 @@ impl Contract { if self.total_voted_stake > total_stake / 3 && self.yes_stake > self.total_voted_stake * 2 / 3 { - self.result = Some(env::block_timestamp_ms()); + self.result = Some(true); Event::ProposalApproved { proposal: &self.proposal, approval_timestamp_ms: &U64::from(env::block_timestamp_ms()), @@ -201,6 +201,18 @@ impl Contract { num_votes: &U64::from(self.votes.len() as u64), } .emit(); + } else { + self.result = Some(false); + Event::ProposalRejected { + proposal: &self.proposal, + rejection_timestamp_ms: &U64::from(env::block_timestamp_ms()), + deadline_timestamp_ms: &U64::from(self.deadline_timestamp_ms), + yes_stake: &U128::from(self.yes_stake), + voted_stake: &U128::from(self.total_voted_stake), + total_stake: &U128::from(total_stake), + num_votes: &U64::from(self.votes.len() as u64), + } + .emit(); } } } @@ -234,8 +246,8 @@ impl Contract { .collect() } - /// Get the timestamp of when the voting finishes. `None` means the voting hasn't ended yet. - pub fn get_result(&self) -> Option { + /// Get the voting result. `None` means the voting hasn't ended yet. + pub fn get_result(&self) -> Option { self.result } @@ -383,9 +395,9 @@ mod tests { #[test] #[should_panic(expected = "Voting has already ended")] - fn test_vote_again_after_voting_ends() { + fn test_ping_again_after_voting_ends() { let validator_id = validator(0); - let context = get_context(&voting_contract_id()); + let mut context = get_context(&voting_contract_id()); let validators = HashMap::from_iter(vec![( validator_id.to_string(), NearToken::from_yoctonear(100), @@ -394,9 +406,20 @@ mod tests { let mut contract = get_contract(); // vote vote(&mut contract, Vote::Yes, &validator_id); - assert!(contract.get_result().is_some()); - // vote again. should panic because voting has ended - vote(&mut contract, Vote::Yes, &validator_id); + // vote result is none because deadline has not passed, + // although all validators have voted + assert!(contract.get_result().is_none()); + + // ping at epoch 2 after deadline + set_context( + context + .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) + .epoch_height(2), + ); + contract.ping(); + + // ping again. should panic because voting has ended + contract.ping(); } #[test] @@ -444,11 +467,7 @@ mod tests { assert_eq!(contract.get_votes(), expected_votes); assert_eq!(contract.get_votes().len() as u64, i + 1); // check voting result - if i < 200 { - assert!(contract.get_result().is_none()); - } else { - assert!(contract.get_result().is_some()); - } + assert!(contract.get_result().is_none()); } } @@ -465,12 +484,8 @@ mod tests { vote(&mut contract, Vote::Yes, &validator(i)); // check votes assert_eq!(contract.get_votes().len() as u64, i + 1); - // check voting result - if i < 200 { - assert!(contract.get_result().is_none()); - } else { - assert!(contract.get_result().is_some()); - } + // voting result is none since deadline has not passed + assert!(contract.get_result().is_none()); } } @@ -491,7 +506,8 @@ mod tests { let context = get_context_with_epoch_height(&voting_contract_id(), 2); set_context_and_validators(&context, &validators); contract.ping(); - assert!(contract.get_result().is_some()); + // voting result is none since deadline has not passed + assert!(contract.get_result().is_none()); } #[test] From 78efe453172c6ea1803ea186fb38dfee482568f0 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 06:57:41 +0000 Subject: [PATCH 05/23] fix: change vote --- src/lib.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 8b6a8d7..18f6e44 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -85,10 +85,7 @@ impl Contract { if voted_stake.vote == Vote::Yes { self.yes_stake += account_current_stake; } - *voted_stake = VotedStake { - vote: voted_stake.vote.clone(), - stake: account_current_stake, - }; + voted_stake.stake = account_current_stake; } self.check_result(); self.last_epoch_height = cur_epoch_height; @@ -137,18 +134,27 @@ impl Contract { self.ping(); - let stake = validator_stake(&account_id); - require!(stake > 0, format!("{} is not a validator", account_id)); + let account_stake = validator_stake(&account_id); + require!( + account_stake > 0, + format!("{} is not a validator", account_id) + ); - let account_stake = match vote { - Vote::Yes => stake, + let account_stake_yes = match vote { + Vote::Yes => account_stake, Vote::No => 0, }; - let prev_stake = if let Some(voted_stake) = self.votes.get(&account_id) { - voted_stake.stake + let (prev_stake, prev_stake_yes) = if let Some(voted_stake) = self.votes.get(&account_id) { + ( + voted_stake.stake, + match voted_stake.vote { + Vote::Yes => voted_stake.stake, + Vote::No => 0, + }, + ) } else { - 0 + (0, 0) }; require!( prev_stake <= self.total_voted_stake, @@ -158,9 +164,7 @@ impl Contract { ) ); self.total_voted_stake = self.total_voted_stake + account_stake - prev_stake; - if vote == Vote::Yes { - self.yes_stake = self.yes_stake + account_stake - prev_stake; - } + self.yes_stake = self.yes_stake + account_stake_yes - prev_stake_yes; self.votes.insert( account_id.clone(), VotedStake { From 40cfc4fbb3536d491e4072e34c361d9b55cfd258 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 07:00:43 +0000 Subject: [PATCH 06/23] style: comment --- src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 18f6e44..b634300 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,8 +44,8 @@ pub struct Contract { proposal: String, deadline_timestamp_ms: Timestamp, votes: HashMap, - yes_stake: Balance, // YES voted stake - total_voted_stake: Balance, // YES + NO + yes_stake: Balance, // stake voted for YES + total_voted_stake: Balance, // total voted stake for YES or NO result: Option, last_epoch_height: EpochHeight, } From 64ce3588a63e466300657b9ce29b944cbaa853c6 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:09:52 +0000 Subject: [PATCH 07/23] test: update unit tests --- src/lib.rs | 63 +++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index b634300..078209a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -415,12 +415,14 @@ mod tests { assert!(contract.get_result().is_none()); // ping at epoch 2 after deadline - set_context( + set_context_and_validators( context .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) .epoch_height(2), + &validators, ); contract.ping(); + assert_eq!(contract.get_result().unwrap(), true); // ping again. should panic because voting has ended contract.ping(); @@ -473,11 +475,20 @@ mod tests { // check voting result assert!(contract.get_result().is_none()); } + + // ping at epoch 2 after deadline + set_context( + context + .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) + .epoch_height(2), + ); + contract.ping(); + assert_eq!(contract.get_result().unwrap(), true); } #[test] fn test_voting_with_epoch_change() { - let context = get_context(&voting_contract_id()); + let mut context = get_context(&voting_contract_id()); set_context(&context); let mut contract = get_contract(); @@ -491,6 +502,15 @@ mod tests { // voting result is none since deadline has not passed assert!(contract.get_result().is_none()); } + + // ping at epoch 2 after deadline + set_context( + context + .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) + .epoch_height(2), + ); + contract.ping(); + assert_eq!(contract.get_result().unwrap(), true); } #[test] @@ -500,18 +520,32 @@ mod tests { (validator(2).to_string(), NearToken::from_yoctonear(10)), (validator(3).to_string(), NearToken::from_yoctonear(10)), ]); - // vote at epoch 1 + let context = get_context_with_epoch_height(&voting_contract_id(), 1); set_context_and_validators(&context, &validators); let mut contract = get_contract(); - vote(&mut contract, Vote::Yes, &validator(1)); + // validator 2 votes YES at epoch 1 + vote(&mut contract, Vote::Yes, &validator(2)); + // validator 3 votes NO at epoch 1 + vote(&mut contract, Vote::No, &validator(3)); + // ping at epoch 2 - validators.insert(validator(1).to_string(), NearToken::from_yoctonear(50)); - let context = get_context_with_epoch_height(&voting_contract_id(), 2); + validators.insert(validator(2).to_string(), NearToken::from_yoctonear(25)); + let mut context = get_context_with_epoch_height(&voting_contract_id(), 2); set_context_and_validators(&context, &validators); contract.ping(); // voting result is none since deadline has not passed assert!(contract.get_result().is_none()); + + // ping at epoch 3 after deadline + set_context_and_validators( + context + .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) + .epoch_height(3), + &validators + ); + contract.ping(); + assert_eq!(contract.get_result().unwrap(), true); } #[test] @@ -523,19 +557,25 @@ mod tests { let context = get_context_with_epoch_height(&voting_contract_id(), 1); set_context_and_validators(&context, &validators); let mut contract = get_contract(); + // vote YES at epoch 1 vote(&mut contract, Vote::Yes, &validator(1)); assert_eq!(contract.get_votes().len(), 1); + assert_eq!(contract.get_total_voted_stake(), (U128(10), U128(10), U128(20))); + // vote NO at epoch 2 let context = get_context_with_epoch_height(&voting_contract_id(), 2); set_context_and_validators(&context, &validators); vote(&mut contract, Vote::No, &validator(1)); assert_eq!(contract.get_votes().len(), 1); + assert_eq!(contract.get_total_voted_stake(), (U128(0), U128(10), U128(20))); + // vote YES at epoch 3 let context = get_context_with_epoch_height(&voting_contract_id(), 3); set_context_and_validators(&context, &validators); vote(&mut contract, Vote::Yes, &validator(1)); assert_eq!(contract.get_votes().len(), 1); + assert_eq!(contract.get_total_voted_stake(), (U128(10), U128(10), U128(20))); } #[test] @@ -548,25 +588,28 @@ mod tests { let context = get_context_with_epoch_height(&voting_contract_id(), 1); set_context_and_validators(&context, &validators); let mut contract = get_contract(); + // vote at epoch 1 vote(&mut contract, Vote::Yes, &validator(1)); - assert_eq!((contract.get_total_voted_stake().0).0, 40); + assert_eq!(contract.get_total_voted_stake(), (U128(40), U128(40), U128(60))); assert_eq!(contract.get_votes().len(), 1); - // remove validator at epoch 2 + + // remove validator 1 at epoch 2, i.e. validator 1 is kicked out validators.remove(&validator(1).to_string()); let context = get_context_with_epoch_height(&voting_contract_id(), 2); set_context_and_validators(&context, &validators); // ping will update total voted stake contract.ping(); - assert_eq!((contract.get_total_voted_stake().0).0, 0); + assert_eq!(contract.get_total_voted_stake(), (U128(0), U128(0), U128(20))); assert_eq!(contract.get_votes().len(), 1); + // validator(1) is back to validator set at epoch 3 validators.insert(validator(1).to_string(), NearToken::from_yoctonear(40)); let context = get_context_with_epoch_height(&voting_contract_id(), 3); set_context_and_validators(&context, &validators); // ping will update total voted stake after validator(1) is back contract.ping(); - assert_eq!((contract.get_total_voted_stake().0).0, 40); + assert_eq!(contract.get_total_voted_stake(), (U128(40), U128(40), U128(60))); assert_eq!(contract.get_votes().len(), 1); } From f405792adf0d2b716d094d02a18afc123f6a525c Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:14:27 +0000 Subject: [PATCH 08/23] style: code format --- src/lib.rs | 42 ++++++++++++++++++++++++++++++------------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 078209a..8393489 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -422,7 +422,7 @@ mod tests { &validators, ); contract.ping(); - assert_eq!(contract.get_result().unwrap(), true); + assert!(contract.get_result().unwrap()); // ping again. should panic because voting has ended contract.ping(); @@ -483,7 +483,7 @@ mod tests { .epoch_height(2), ); contract.ping(); - assert_eq!(contract.get_result().unwrap(), true); + assert!(contract.get_result().unwrap()); } #[test] @@ -510,7 +510,7 @@ mod tests { .epoch_height(2), ); contract.ping(); - assert_eq!(contract.get_result().unwrap(), true); + assert!(contract.get_result().unwrap()); } #[test] @@ -528,7 +528,7 @@ mod tests { vote(&mut contract, Vote::Yes, &validator(2)); // validator 3 votes NO at epoch 1 vote(&mut contract, Vote::No, &validator(3)); - + // ping at epoch 2 validators.insert(validator(2).to_string(), NearToken::from_yoctonear(25)); let mut context = get_context_with_epoch_height(&voting_contract_id(), 2); @@ -542,10 +542,10 @@ mod tests { context .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) .epoch_height(3), - &validators + &validators, ); contract.ping(); - assert_eq!(contract.get_result().unwrap(), true); + assert!(contract.get_result().unwrap()); } #[test] @@ -561,21 +561,30 @@ mod tests { // vote YES at epoch 1 vote(&mut contract, Vote::Yes, &validator(1)); assert_eq!(contract.get_votes().len(), 1); - assert_eq!(contract.get_total_voted_stake(), (U128(10), U128(10), U128(20))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(10), U128(10), U128(20)) + ); // vote NO at epoch 2 let context = get_context_with_epoch_height(&voting_contract_id(), 2); set_context_and_validators(&context, &validators); vote(&mut contract, Vote::No, &validator(1)); assert_eq!(contract.get_votes().len(), 1); - assert_eq!(contract.get_total_voted_stake(), (U128(0), U128(10), U128(20))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(0), U128(10), U128(20)) + ); // vote YES at epoch 3 let context = get_context_with_epoch_height(&voting_contract_id(), 3); set_context_and_validators(&context, &validators); vote(&mut contract, Vote::Yes, &validator(1)); assert_eq!(contract.get_votes().len(), 1); - assert_eq!(contract.get_total_voted_stake(), (U128(10), U128(10), U128(20))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(10), U128(10), U128(20)) + ); } #[test] @@ -591,7 +600,10 @@ mod tests { // vote at epoch 1 vote(&mut contract, Vote::Yes, &validator(1)); - assert_eq!(contract.get_total_voted_stake(), (U128(40), U128(40), U128(60))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(40), U128(40), U128(60)) + ); assert_eq!(contract.get_votes().len(), 1); // remove validator 1 at epoch 2, i.e. validator 1 is kicked out @@ -600,7 +612,10 @@ mod tests { set_context_and_validators(&context, &validators); // ping will update total voted stake contract.ping(); - assert_eq!(contract.get_total_voted_stake(), (U128(0), U128(0), U128(20))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(0), U128(0), U128(20)) + ); assert_eq!(contract.get_votes().len(), 1); // validator(1) is back to validator set at epoch 3 @@ -609,7 +624,10 @@ mod tests { set_context_and_validators(&context, &validators); // ping will update total voted stake after validator(1) is back contract.ping(); - assert_eq!(contract.get_total_voted_stake(), (U128(40), U128(40), U128(60))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(40), U128(40), U128(60)) + ); assert_eq!(contract.get_votes().len(), 1); } From 1b5f526c92fed4319673738897680c171dc22423 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:44:51 +0800 Subject: [PATCH 09/23] test: more unit tests --- src/lib.rs | 109 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 8393489..ebcd23f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -688,4 +688,113 @@ mod tests { // has vote result assert!(contract.get_result().is_some()); } + + #[test] + fn test_proposal_rejected_since_not_enough_total_voted_stake() { + let mut validators: HashMap = HashMap::from_iter(vec![ + (validator(1).to_string(), NearToken::from_yoctonear(50)), + (validator(2).to_string(), NearToken::from_yoctonear(30)), + (validator(3).to_string(), NearToken::from_yoctonear(30)), + (validator(4).to_string(), NearToken::from_yoctonear(10)), + ]); + let context = get_context_with_epoch_height(&voting_contract_id(), 1); + set_context_and_validators(&context, &validators); + let mut contract = get_contract(); + + // vote at epoch 1 + set_context(&context); + vote(&mut contract, Vote::Yes, &validator(3)); + vote(&mut contract, Vote::No, &validator(4)); + + // ping at epoch 2 after deadline + set_context( + context + .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) + .epoch_height(2), + ); + contract.ping(); + assert_eq!(contract.get_result().wrap(), false); + } + + #[test] + fn test_proposal_rejected_since_not_enough_total_voted_stake() { + let mut validators: HashMap = HashMap::from_iter(vec![ + (validator(1).to_string(), NearToken::from_yoctonear(50)), + (validator(2).to_string(), NearToken::from_yoctonear(30)), + (validator(3).to_string(), NearToken::from_yoctonear(30)), + (validator(4).to_string(), NearToken::from_yoctonear(10)), + ]); + let mut context = get_context_with_epoch_height(&voting_contract_id(), 1); + set_context_and_validators(&context, &validators); + let mut contract = get_contract(); + + // vote at epoch 1 + vote(&mut contract, Vote::Yes, &validator(3)); + vote(&mut contract, Vote::No, &validator(4)); + + // ping at epoch 2 after deadline + set_context( + context + .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) + .epoch_height(2), + &validators, + ); + contract.ping(); + assert_eq!(contract.get_result().wrap(), false); + } + + #[test] + fn test_proposal_rejected_since_not_enough_yes_stake() { + let mut validators: HashMap = HashMap::from_iter(vec![ + (validator(1).to_string(), NearToken::from_yoctonear(50)), + (validator(2).to_string(), NearToken::from_yoctonear(30)), + (validator(3).to_string(), NearToken::from_yoctonear(30)), + (validator(4).to_string(), NearToken::from_yoctonear(10)), + ]); + let mut context = get_context_with_epoch_height(&voting_contract_id(), 1); + set_context_and_validators(&context, &validators); + let mut contract = get_contract(); + + // vote at epoch 1 + vote(&mut contract, Vote::Yes, &validator(1)); + vote(&mut contract, Vote::No, &validator(2)); + + // ping at epoch 2 after deadline + set_context( + context + .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) + .epoch_height(2), + &validators, + ); + contract.ping(); + assert_eq!(contract.get_result().wrap(), false); + } + + #[test] + fn test_proposal_approved() { + let mut validators: HashMap = HashMap::from_iter(vec![ + (validator(1).to_string(), NearToken::from_yoctonear(50)), + (validator(2).to_string(), NearToken::from_yoctonear(30)), + (validator(3).to_string(), NearToken::from_yoctonear(30)), + (validator(4).to_string(), NearToken::from_yoctonear(10)), + ]); + let mut context = get_context_with_epoch_height(&voting_contract_id(), 1); + set_context_and_validators(&context, &validators); + let mut contract = get_contract(); + + // vote at epoch 1 + vote(&mut contract, Vote::Yes, &validator(1)); + vote(&mut contract, Vote::Yes, &validator(2)); + vote(&mut contract, Vote::No, &validator(4)); + + // ping at epoch 2 after deadline + set_context( + context + .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) + .epoch_height(2), + &validators, + ); + contract.ping(); + assert_eq!(contract.get_result().wrap(), true); + } } From 58f801bb063222b57b468c54188e6778b0b578e5 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 16:52:21 +0800 Subject: [PATCH 10/23] test: unit tests --- src/lib.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index ebcd23f..109377a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -731,6 +731,8 @@ mod tests { // vote at epoch 1 vote(&mut contract, Vote::Yes, &validator(3)); vote(&mut contract, Vote::No, &validator(4)); + assert_eq!(contract.get_votes().len(), 2); + assert_eq!(contract.get_total_voted_stake(), (U128(30), U128(40), U128(120))); // ping at epoch 2 after deadline set_context( @@ -758,6 +760,8 @@ mod tests { // vote at epoch 1 vote(&mut contract, Vote::Yes, &validator(1)); vote(&mut contract, Vote::No, &validator(2)); + assert_eq!(contract.get_votes().len(), 2); + assert_eq!(contract.get_total_voted_stake(), (U128(50), U128(80), U128(120))); // ping at epoch 2 after deadline set_context( @@ -777,6 +781,7 @@ mod tests { (validator(2).to_string(), NearToken::from_yoctonear(30)), (validator(3).to_string(), NearToken::from_yoctonear(30)), (validator(4).to_string(), NearToken::from_yoctonear(10)), + (validator(4).to_string(), NearToken::from_yoctonear(20)), ]); let mut context = get_context_with_epoch_height(&voting_contract_id(), 1); set_context_and_validators(&context, &validators); @@ -786,12 +791,20 @@ mod tests { vote(&mut contract, Vote::Yes, &validator(1)); vote(&mut contract, Vote::Yes, &validator(2)); vote(&mut contract, Vote::No, &validator(4)); + assert_eq!(contract.get_votes().len(), 3); + assert_eq!(contract.get_total_voted_stake(), (U128(80), U128(90), U128(140))); - // ping at epoch 2 after deadline + // vote at epoch 2 + vote(&mut contract, Vote::No, &validator(3)); + vote(&mut contract, Vote::Yes, &validator(4)); + assert_eq!(contract.get_votes().len(), 3); + assert_eq!(contract.get_total_voted_stake(), (U128(90), U128(120), U128(140))); + + // ping at epoch 3 after deadline set_context( context .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) - .epoch_height(2), + .epoch_height(3), &validators, ); contract.ping(); From c07ae610d27e2fbae818d1fad10621c4df410d7b Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 08:59:43 +0000 Subject: [PATCH 11/23] test: unit tests --- src/lib.rs | 56 ++++++++++++++---------------------------------------- 1 file changed, 14 insertions(+), 42 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 109377a..080c9c4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -45,8 +45,8 @@ pub struct Contract { deadline_timestamp_ms: Timestamp, votes: HashMap, yes_stake: Balance, // stake voted for YES - total_voted_stake: Balance, // total voted stake for YES or NO - result: Option, + total_voted_stake: Balance, // total stake voted for YES or NO + result: Option, // true if proposal is approved last_epoch_height: EpochHeight, } @@ -691,34 +691,7 @@ mod tests { #[test] fn test_proposal_rejected_since_not_enough_total_voted_stake() { - let mut validators: HashMap = HashMap::from_iter(vec![ - (validator(1).to_string(), NearToken::from_yoctonear(50)), - (validator(2).to_string(), NearToken::from_yoctonear(30)), - (validator(3).to_string(), NearToken::from_yoctonear(30)), - (validator(4).to_string(), NearToken::from_yoctonear(10)), - ]); - let context = get_context_with_epoch_height(&voting_contract_id(), 1); - set_context_and_validators(&context, &validators); - let mut contract = get_contract(); - - // vote at epoch 1 - set_context(&context); - vote(&mut contract, Vote::Yes, &validator(3)); - vote(&mut contract, Vote::No, &validator(4)); - - // ping at epoch 2 after deadline - set_context( - context - .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) - .epoch_height(2), - ); - contract.ping(); - assert_eq!(contract.get_result().wrap(), false); - } - - #[test] - fn test_proposal_rejected_since_not_enough_total_voted_stake() { - let mut validators: HashMap = HashMap::from_iter(vec![ + let validators: HashMap = HashMap::from_iter(vec![ (validator(1).to_string(), NearToken::from_yoctonear(50)), (validator(2).to_string(), NearToken::from_yoctonear(30)), (validator(3).to_string(), NearToken::from_yoctonear(30)), @@ -729,25 +702,24 @@ mod tests { let mut contract = get_contract(); // vote at epoch 1 + set_context(&context); vote(&mut contract, Vote::Yes, &validator(3)); vote(&mut contract, Vote::No, &validator(4)); - assert_eq!(contract.get_votes().len(), 2); - assert_eq!(contract.get_total_voted_stake(), (U128(30), U128(40), U128(120))); // ping at epoch 2 after deadline - set_context( + set_context_and_validators( context .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) .epoch_height(2), &validators, ); contract.ping(); - assert_eq!(contract.get_result().wrap(), false); + assert_eq!(contract.get_result().unwrap(), false); } #[test] fn test_proposal_rejected_since_not_enough_yes_stake() { - let mut validators: HashMap = HashMap::from_iter(vec![ + let validators: HashMap = HashMap::from_iter(vec![ (validator(1).to_string(), NearToken::from_yoctonear(50)), (validator(2).to_string(), NearToken::from_yoctonear(30)), (validator(3).to_string(), NearToken::from_yoctonear(30)), @@ -764,24 +736,24 @@ mod tests { assert_eq!(contract.get_total_voted_stake(), (U128(50), U128(80), U128(120))); // ping at epoch 2 after deadline - set_context( + set_context_and_validators( context .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) .epoch_height(2), &validators, ); contract.ping(); - assert_eq!(contract.get_result().wrap(), false); + assert_eq!(contract.get_result().unwrap(), false); } #[test] fn test_proposal_approved() { - let mut validators: HashMap = HashMap::from_iter(vec![ + let validators: HashMap = HashMap::from_iter(vec![ (validator(1).to_string(), NearToken::from_yoctonear(50)), (validator(2).to_string(), NearToken::from_yoctonear(30)), (validator(3).to_string(), NearToken::from_yoctonear(30)), (validator(4).to_string(), NearToken::from_yoctonear(10)), - (validator(4).to_string(), NearToken::from_yoctonear(20)), + (validator(5).to_string(), NearToken::from_yoctonear(20)), ]); let mut context = get_context_with_epoch_height(&voting_contract_id(), 1); set_context_and_validators(&context, &validators); @@ -797,17 +769,17 @@ mod tests { // vote at epoch 2 vote(&mut contract, Vote::No, &validator(3)); vote(&mut contract, Vote::Yes, &validator(4)); - assert_eq!(contract.get_votes().len(), 3); + assert_eq!(contract.get_votes().len(), 4); assert_eq!(contract.get_total_voted_stake(), (U128(90), U128(120), U128(140))); // ping at epoch 3 after deadline - set_context( + set_context_and_validators( context .block_timestamp(env::block_timestamp_ms() + 2000 * 1_000_000) .epoch_height(3), &validators, ); contract.ping(); - assert_eq!(contract.get_result().wrap(), true); + assert_eq!(contract.get_result().unwrap(), true); } } From ad7969ee7f5f5edaaa981a17242df329d67c838a Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:17:04 +0000 Subject: [PATCH 12/23] test: integration test --- src/lib.rs | 1 + tests/test_vote.rs | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 080c9c4..0274c9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -267,6 +267,7 @@ impl Contract { } #[cfg(feature = "test")] +/// Test methods #[near] impl Contract { pub fn set_validator_stake(&mut self, validator_account_id: AccountId, amount: U128) { diff --git a/tests/test_vote.rs b/tests/test_vote.rs index 50e455d..7861c51 100644 --- a/tests/test_vote.rs +++ b/tests/test_vote.rs @@ -1,5 +1,7 @@ +use near_sdk::json_types::U128; use near_sdk::{AccountId, Gas, NearToken}; use serde_json::json; +use validator_voting::Vote; use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; @@ -157,7 +159,7 @@ async fn test_many_votes() -> Result<(), Box> { println!( "total staked: {}, {:#?}", alice.id(), - total_staked.json::()? + total_staked.json::()? ); } @@ -206,7 +208,7 @@ async fn test_many_votes() -> Result<(), Box> { voting_contract .view("get_votes") .await? - .json::>()? + .json::>()? ); } @@ -313,7 +315,7 @@ async fn test_vote_no_after_vote_yes() -> Result<(), Box> ); let votes = owner.view(voting_contract.id(), "get_votes").await?; - assert_eq!(votes.json::>()?.len(), 1); + assert_eq!(votes.json::>()?.len(), 1); let outcome = owner .call(voting_contract.id(), "vote") @@ -332,7 +334,7 @@ async fn test_vote_no_after_vote_yes() -> Result<(), Box> ); let votes = owner.view(voting_contract.id(), "get_votes").await?; - assert_eq!(votes.json::>()?.len(), 0); + assert_eq!(votes.json::>()?.len(), 1); Ok(()) } @@ -384,7 +386,7 @@ async fn test_vote_yes_after_vote_no() -> Result<(), Box> ); let votes = owner.view(voting_contract.id(), "get_votes").await?; - assert_eq!(votes.json::>()?.len(), 0); + assert_eq!(votes.json::>()?.len(), 1); let outcome = owner .call(voting_contract.id(), "vote") @@ -403,7 +405,7 @@ async fn test_vote_yes_after_vote_no() -> Result<(), Box> ); let votes = owner.view(voting_contract.id(), "get_votes").await?; - assert_eq!(votes.json::>()?.len(), 1); + assert_eq!(votes.json::>()?.len(), 1); Ok(()) } @@ -469,7 +471,7 @@ async fn test_unstake_after_voting() -> Result<(), Box> { ); let votes = owner.view(voting_contract.id(), "get_votes").await?; - let votes = votes.json::>()?; + let votes = votes.json::>()?; assert_eq!(votes.len(), 1); assert!(votes.contains_key(staking_pool_contracts[0].id())); @@ -492,7 +494,7 @@ async fn test_unstake_after_voting() -> Result<(), Box> { ); let votes = owner.view(voting_contract.id(), "get_votes").await?; - let votes = votes.json::>()?; + let votes = votes.json::>()?; assert_eq!(votes.len(), 2); assert!(votes.contains_key(staking_pool_contracts[1].id())); From b01dd9f864af18ea155373f8728b3165a5b8844a Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:22:38 +0000 Subject: [PATCH 13/23] style: code format --- src/lib.rs | 21 +++++++++++++++------ tests/test_vote.rs | 2 +- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 0274c9d..39281de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -715,7 +715,7 @@ mod tests { &validators, ); contract.ping(); - assert_eq!(contract.get_result().unwrap(), false); + assert!(!contract.get_result().unwrap()); } #[test] @@ -734,7 +734,10 @@ mod tests { vote(&mut contract, Vote::Yes, &validator(1)); vote(&mut contract, Vote::No, &validator(2)); assert_eq!(contract.get_votes().len(), 2); - assert_eq!(contract.get_total_voted_stake(), (U128(50), U128(80), U128(120))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(50), U128(80), U128(120)) + ); // ping at epoch 2 after deadline set_context_and_validators( @@ -744,7 +747,7 @@ mod tests { &validators, ); contract.ping(); - assert_eq!(contract.get_result().unwrap(), false); + assert!(!contract.get_result().unwrap()); } #[test] @@ -765,13 +768,19 @@ mod tests { vote(&mut contract, Vote::Yes, &validator(2)); vote(&mut contract, Vote::No, &validator(4)); assert_eq!(contract.get_votes().len(), 3); - assert_eq!(contract.get_total_voted_stake(), (U128(80), U128(90), U128(140))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(80), U128(90), U128(140)) + ); // vote at epoch 2 vote(&mut contract, Vote::No, &validator(3)); vote(&mut contract, Vote::Yes, &validator(4)); assert_eq!(contract.get_votes().len(), 4); - assert_eq!(contract.get_total_voted_stake(), (U128(90), U128(120), U128(140))); + assert_eq!( + contract.get_total_voted_stake(), + (U128(90), U128(120), U128(140)) + ); // ping at epoch 3 after deadline set_context_and_validators( @@ -781,6 +790,6 @@ mod tests { &validators, ); contract.ping(); - assert_eq!(contract.get_result().unwrap(), true); + assert!(contract.get_result().unwrap()); } } diff --git a/tests/test_vote.rs b/tests/test_vote.rs index 7861c51..721eace 100644 --- a/tests/test_vote.rs +++ b/tests/test_vote.rs @@ -1,9 +1,9 @@ use near_sdk::json_types::U128; use near_sdk::{AccountId, Gas, NearToken}; use serde_json::json; -use validator_voting::Vote; use std::collections::HashMap; use std::time::{SystemTime, UNIX_EPOCH}; +use validator_voting::Vote; mod utils; use utils::*; From a2892753d73747bcd3b805bb1d216c3c0d1f0a08 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:58:02 +0000 Subject: [PATCH 14/23] chore: update test scripts --- tests/scripts/create-proposal.sh | 8 ++++---- tests/scripts/create-validators.sh | 8 ++++---- tests/scripts/vote.sh | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/scripts/create-proposal.sh b/tests/scripts/create-proposal.sh index ed21d19..a96fe02 100644 --- a/tests/scripts/create-proposal.sh +++ b/tests/scripts/create-proposal.sh @@ -1,10 +1,10 @@ #!/bin/bash -export VOTING_ACCOUNT_ID=reduce-inflation.testnet +export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet export PROPOSAL_DESCRIPTION="reduce inflation rate" -export DEADLINE_TIMESTAMP=1749715200000 +export DEADLINE_TIMESTAMP=1753257600000 # create account -near account create-account sponsor-by-faucet-service $VOTING_ACCOUNT_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create +# near account create-account sponsor-by-faucet-service $VOTING_ACCOUNT_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create # deploy contract -near contract deploy $VOTING_ACCOUNT_ID use-file validator_voting.wasm with-init-call new json-args '{"proposal":"'$PROPOSAL_DESCRIPTION'","deadline_timestamp_ms":'$DEADLINE_TIMESTAMP'}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send +near contract deploy $VOTING_ACCOUNT_ID use-file ./res/validator_voting.wasm with-init-call new json-args '{"proposal":"'$PROPOSAL_DESCRIPTION'","deadline_timestamp_ms":'$DEADLINE_TIMESTAMP'}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send diff --git a/tests/scripts/create-validators.sh b/tests/scripts/create-validators.sh index b613fcc..8f76922 100644 --- a/tests/scripts/create-validators.sh +++ b/tests/scripts/create-validators.sh @@ -1,16 +1,16 @@ #!/bin/bash export OWNER_ID=mock-owner.testnet export STAKE_ACCOUNT_ID=mock-staker.testnet -export VOTING_ACCOUNT_ID=reduce-inflation.testnet +export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet export STAKE_PUBLIC_KEY=ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp -for i in {1..1}; do - VALIDATOR_ID="mock-node-"${i}".testnet" +for i in {4..10}; do + VALIDATOR_ID="mock-vali-"${i}".testnet" # create validator account # near account create-account sponsor-by-faucet-service $VALIDATOR_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create near account create-account fund-myself $VALIDATOR_ID '2 NEAR' autogenerate-new-keypair save-to-legacy-keychain sign-as $STAKE_ACCOUNT_ID network-config testnet sign-with-legacy-keychain send # deploy mock staking pool contract - near contract deploy $VALIDATOR_ID use-file mock_staking_pool.wasm with-init-call new json-args '{"owner_id":"'$OWNER_ID'","stake_public_key":"'$STAKE_PUBLIC_KEY'","voting_account_id":"'$VOTING_ACCOUNT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send + near contract deploy $VALIDATOR_ID use-file ./contracts/mock-staking-pool/target/near/mock_staking_pool.wasm with-init-call new json-args '{"owner_id":"'$OWNER_ID'","stake_public_key":"'$STAKE_PUBLIC_KEY'","voting_account_id":"'$VOTING_ACCOUNT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send # stake some NEAR to near contract call-function as-transaction $VALIDATOR_ID deposit_and_stake json-args {} prepaid-gas '200.0 Tgas' attached-deposit '1 NEAR' sign-as $STAKE_ACCOUNT_ID network-config testnet sign-with-legacy-keychain send done diff --git a/tests/scripts/vote.sh b/tests/scripts/vote.sh index 715ee7c..da0349f 100644 --- a/tests/scripts/vote.sh +++ b/tests/scripts/vote.sh @@ -1,11 +1,11 @@ #!/bin/bash export OWNER_ID=mock-owner.testnet -export VOTING_ACCOUNT_ID=mock-proposal.testnet +export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet -for i in {101..200}; do - VALIDATOR_ID="mock-validator-"${i}".testnet" +for i in {1..1}; do + VALIDATOR_ID="mock-vali-"${i}".testnet" # vote by validator - near contract call-function as-transaction $VALIDATOR_ID vote json-args '{"voting_account_id":"'$VOTING_ACCOUNT_ID'","is_vote":true}' prepaid-gas '200.0 Tgas' attached-deposit '0 NEAR' sign-as $OWNER_ID network-config testnet sign-with-legacy-keychain send + near contract call-function as-transaction $VOTING_ACCOUNT_ID vote json-args '{"staking_pool_id":"'$VALIDATOR_ID'","vote":"no"}' prepaid-gas '200.0 Tgas' attached-deposit '0 NEAR' sign-as $OWNER_ID network-config testnet sign-with-legacy-keychain send done # get total voted stake From 3b2ed011e97956a026396f1ac0a16e995eca8d98 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 09:59:46 +0000 Subject: [PATCH 15/23] chore: update scripts --- tests/scripts/create-validators.sh | 2 +- tests/scripts/vote.sh | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/scripts/create-validators.sh b/tests/scripts/create-validators.sh index 8f76922..4a1c531 100644 --- a/tests/scripts/create-validators.sh +++ b/tests/scripts/create-validators.sh @@ -4,7 +4,7 @@ export STAKE_ACCOUNT_ID=mock-staker.testnet export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet export STAKE_PUBLIC_KEY=ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp -for i in {4..10}; do +for i in {1..10}; do VALIDATOR_ID="mock-vali-"${i}".testnet" # create validator account # near account create-account sponsor-by-faucet-service $VALIDATOR_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create diff --git a/tests/scripts/vote.sh b/tests/scripts/vote.sh index da0349f..19affc5 100644 --- a/tests/scripts/vote.sh +++ b/tests/scripts/vote.sh @@ -2,10 +2,10 @@ export OWNER_ID=mock-owner.testnet export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet -for i in {1..1}; do +for i in {1..10}; do VALIDATOR_ID="mock-vali-"${i}".testnet" # vote by validator - near contract call-function as-transaction $VOTING_ACCOUNT_ID vote json-args '{"staking_pool_id":"'$VALIDATOR_ID'","vote":"no"}' prepaid-gas '200.0 Tgas' attached-deposit '0 NEAR' sign-as $OWNER_ID network-config testnet sign-with-legacy-keychain send + near contract call-function as-transaction $VOTING_ACCOUNT_ID vote json-args '{"staking_pool_id":"'$VALIDATOR_ID'","vote":"yes"}' prepaid-gas '200.0 Tgas' attached-deposit '0 NEAR' sign-as $OWNER_ID network-config testnet sign-with-legacy-keychain send done # get total voted stake From 753dcd9a943a8d6772bbeb9ef2f57ba744d93360 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:04:18 +0000 Subject: [PATCH 16/23] chore: updae scripts --- tests/scripts/create-proposal.sh | 5 +++-- tests/scripts/create-validators.sh | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/scripts/create-proposal.sh b/tests/scripts/create-proposal.sh index a96fe02..0b8f8ed 100644 --- a/tests/scripts/create-proposal.sh +++ b/tests/scripts/create-proposal.sh @@ -1,10 +1,11 @@ #!/bin/bash +export MOCK_PROPOSAL_CONTRACT=./res/validator_voting.wasm export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet export PROPOSAL_DESCRIPTION="reduce inflation rate" export DEADLINE_TIMESTAMP=1753257600000 # create account -# near account create-account sponsor-by-faucet-service $VOTING_ACCOUNT_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create +near account create-account sponsor-by-faucet-service $VOTING_ACCOUNT_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create # deploy contract -near contract deploy $VOTING_ACCOUNT_ID use-file ./res/validator_voting.wasm with-init-call new json-args '{"proposal":"'$PROPOSAL_DESCRIPTION'","deadline_timestamp_ms":'$DEADLINE_TIMESTAMP'}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send +near contract deploy $VOTING_ACCOUNT_ID use-file $MOCK_PROPOSAL_CONTRACT with-init-call new json-args '{"proposal":"'$PROPOSAL_DESCRIPTION'","deadline_timestamp_ms":'$DEADLINE_TIMESTAMP'}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send diff --git a/tests/scripts/create-validators.sh b/tests/scripts/create-validators.sh index 4a1c531..6c4a35d 100644 --- a/tests/scripts/create-validators.sh +++ b/tests/scripts/create-validators.sh @@ -1,4 +1,5 @@ #!/bin/bash +export MOCK_STAKING_POOL_CONTRACT=./contracts/mock-staking-pool/target/near/mock_staking_pool.wasm export OWNER_ID=mock-owner.testnet export STAKE_ACCOUNT_ID=mock-staker.testnet export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet @@ -10,7 +11,7 @@ for i in {1..10}; do # near account create-account sponsor-by-faucet-service $VALIDATOR_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create near account create-account fund-myself $VALIDATOR_ID '2 NEAR' autogenerate-new-keypair save-to-legacy-keychain sign-as $STAKE_ACCOUNT_ID network-config testnet sign-with-legacy-keychain send # deploy mock staking pool contract - near contract deploy $VALIDATOR_ID use-file ./contracts/mock-staking-pool/target/near/mock_staking_pool.wasm with-init-call new json-args '{"owner_id":"'$OWNER_ID'","stake_public_key":"'$STAKE_PUBLIC_KEY'","voting_account_id":"'$VOTING_ACCOUNT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send + near contract deploy $VALIDATOR_ID use-file $MOCK_STAKING_POOL_CONTRACT with-init-call new json-args '{"owner_id":"'$OWNER_ID'","stake_public_key":"'$STAKE_PUBLIC_KEY'","voting_account_id":"'$VOTING_ACCOUNT_ID'"}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send # stake some NEAR to near contract call-function as-transaction $VALIDATOR_ID deposit_and_stake json-args {} prepaid-gas '200.0 Tgas' attached-deposit '1 NEAR' sign-as $STAKE_ACCOUNT_ID network-config testnet sign-with-legacy-keychain send done From 2dc349195d2ea5950cadcb9ef8df9667db5431fd Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 18:20:30 +0800 Subject: [PATCH 17/23] feat: number of votes --- src/events.rs | 6 ++++-- src/lib.rs | 7 +++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/events.rs b/src/events.rs index 74051c3..0c0a408 100644 --- a/src/events.rs +++ b/src/events.rs @@ -27,7 +27,8 @@ pub enum Event<'a> { yes_stake: &'a U128, voted_stake: &'a U128, total_stake: &'a U128, - num_votes: &'a U64, + num_votes_yes: &'a U64, + num_votes_total: &'a U64, }, ProposalRejected { proposal: &'a String, @@ -36,7 +37,8 @@ pub enum Event<'a> { yes_stake: &'a U128, voted_stake: &'a U128, total_stake: &'a U128, - num_votes: &'a U64, + num_votes_yes: &'a U64, + num_votes_total: &'a U64, }, } diff --git a/src/lib.rs b/src/lib.rs index 39281de..da306cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,6 +191,7 @@ impl Contract { return; } let total_stake = validator_total_stake(); + let num_votes_yes = self.votes.iter().filter(|(_, v)| v.vote == Vote::Yes).count() as u64; if self.total_voted_stake > total_stake / 3 && self.yes_stake > self.total_voted_stake * 2 / 3 { @@ -202,7 +203,8 @@ impl Contract { yes_stake: &U128::from(self.yes_stake), voted_stake: &U128::from(self.total_voted_stake), total_stake: &U128::from(total_stake), - num_votes: &U64::from(self.votes.len() as u64), + num_votes_yes: &U64::from(num_votes_yes), + num_votes_total: &U64::from(self.votes.len() as u64), } .emit(); } else { @@ -214,7 +216,8 @@ impl Contract { yes_stake: &U128::from(self.yes_stake), voted_stake: &U128::from(self.total_voted_stake), total_stake: &U128::from(total_stake), - num_votes: &U64::from(self.votes.len() as u64), + num_votes_yes: &U64::from(num_votes_yes), + num_votes_total: &U64::from(self.votes.len() as u64), } .emit(); } From 0aa44ab348dea1387afb60eb2e598da6817adabe Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Tue, 22 Jul 2025 10:33:26 +0000 Subject: [PATCH 18/23] style: code format --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index da306cb..89036c6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -191,7 +191,11 @@ impl Contract { return; } let total_stake = validator_total_stake(); - let num_votes_yes = self.votes.iter().filter(|(_, v)| v.vote == Vote::Yes).count() as u64; + let num_votes_yes = self + .votes + .iter() + .filter(|(_, v)| v.vote == Vote::Yes) + .count() as u64; if self.total_voted_stake > total_stake / 3 && self.yes_stake > self.total_voted_stake * 2 / 3 { From ea1290b36c4e15eb9fd9609c59d540f3a5840dbf Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Wed, 23 Jul 2025 03:55:10 +0000 Subject: [PATCH 19/23] fix: check result for each ping call --- src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 89036c6..2a60fa9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -87,9 +87,9 @@ impl Contract { } voted_stake.stake = account_current_stake; } - self.check_result(); self.last_epoch_height = cur_epoch_height; } + self.check_result(); } /// Method for validators to vote with `Yes` or `No`. From b92179ad04000405c5ed9e6ba488972f44bceeaa Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Wed, 23 Jul 2025 08:22:30 +0000 Subject: [PATCH 20/23] docs: commands --- README.md | 4 ++-- makefile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 435df04..80124f6 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,13 @@ The purpose of this contract is for validators to vote on any specific proposal. Install [`cargo-near`](https://github.com/near/cargo-near) and run: ```bash -cargo near build +make ``` ## Test ```bash -cargo test +make test ``` ## Deploy diff --git a/makefile b/makefile index 03fdc4e..811bca2 100644 --- a/makefile +++ b/makefile @@ -1,6 +1,6 @@ RUSTFLAGS = "-C link-arg=-s" -all: lint validator-voting validator-voting-test +all: lint validator-voting lint: @cargo fmt --all From 26f1aef9042d228d5f94d4b3ee136a1ece76bd3c Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Wed, 23 Jul 2025 09:41:18 +0000 Subject: [PATCH 21/23] chore: update scripts --- tests/scripts/create-proposal.sh | 10 +++++++--- tests/scripts/create-validators.sh | 5 +++-- tests/scripts/vote.sh | 7 ++++--- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/tests/scripts/create-proposal.sh b/tests/scripts/create-proposal.sh index 0b8f8ed..ba84cb4 100644 --- a/tests/scripts/create-proposal.sh +++ b/tests/scripts/create-proposal.sh @@ -1,11 +1,15 @@ #!/bin/bash +export SUFFIX=delta export MOCK_PROPOSAL_CONTRACT=./res/validator_voting.wasm -export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet +export VOTING_ACCOUNT_ID="mock-proposal-"$SUFFIX".testnet" export PROPOSAL_DESCRIPTION="reduce inflation rate" -export DEADLINE_TIMESTAMP=1753257600000 +export DEADLINE_TIMESTAMP=1753430400000 +export INIT_ARGS='{"proposal":"'$PROPOSAL_DESCRIPTION'","deadline_timestamp_ms":'$DEADLINE_TIMESTAMP'}' + +echo $ARGS # create account near account create-account sponsor-by-faucet-service $VOTING_ACCOUNT_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create # deploy contract -near contract deploy $VOTING_ACCOUNT_ID use-file $MOCK_PROPOSAL_CONTRACT with-init-call new json-args '{"proposal":"'$PROPOSAL_DESCRIPTION'","deadline_timestamp_ms":'$DEADLINE_TIMESTAMP'}' prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send +near contract deploy $VOTING_ACCOUNT_ID use-file $MOCK_PROPOSAL_CONTRACT with-init-call new json-args "$INIT_ARGS" prepaid-gas '100.0 Tgas' attached-deposit '0 NEAR' network-config testnet sign-with-legacy-keychain send diff --git a/tests/scripts/create-validators.sh b/tests/scripts/create-validators.sh index 6c4a35d..62986f5 100644 --- a/tests/scripts/create-validators.sh +++ b/tests/scripts/create-validators.sh @@ -1,12 +1,13 @@ #!/bin/bash +export SUFFIX=delta export MOCK_STAKING_POOL_CONTRACT=./contracts/mock-staking-pool/target/near/mock_staking_pool.wasm export OWNER_ID=mock-owner.testnet export STAKE_ACCOUNT_ID=mock-staker.testnet -export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet +export VOTING_ACCOUNT_ID="mock-proposal-"$SUFFIX".testnet" export STAKE_PUBLIC_KEY=ed25519:6E8sCci9badyRkXb3JoRpBj5p8C6Tw41ELDZoiihKEtp for i in {1..10}; do - VALIDATOR_ID="mock-vali-"${i}".testnet" + VALIDATOR_ID="mock-node-"$SUFFIX"-"${i}".testnet" # create validator account # near account create-account sponsor-by-faucet-service $VALIDATOR_ID autogenerate-new-keypair save-to-legacy-keychain network-config testnet create near account create-account fund-myself $VALIDATOR_ID '2 NEAR' autogenerate-new-keypair save-to-legacy-keychain sign-as $STAKE_ACCOUNT_ID network-config testnet sign-with-legacy-keychain send diff --git a/tests/scripts/vote.sh b/tests/scripts/vote.sh index 19affc5..aa4cd73 100644 --- a/tests/scripts/vote.sh +++ b/tests/scripts/vote.sh @@ -1,9 +1,10 @@ #!/bin/bash +export SUFFIX=delta export OWNER_ID=mock-owner.testnet -export VOTING_ACCOUNT_ID=mock-proposal-alpha.testnet +export VOTING_ACCOUNT_ID="mock-proposal-"$SUFFIX".testnet" -for i in {1..10}; do - VALIDATOR_ID="mock-vali-"${i}".testnet" +for i in {1..1}; do + VALIDATOR_ID="mock-node-"$SUFFIX"-"${i}".testnet" # vote by validator near contract call-function as-transaction $VOTING_ACCOUNT_ID vote json-args '{"staking_pool_id":"'$VALIDATOR_ID'","vote":"yes"}' prepaid-gas '200.0 Tgas' attached-deposit '0 NEAR' sign-as $OWNER_ID network-config testnet sign-with-legacy-keychain send done From 78ce1cb1f46d84109da59d54b95504ef8b512421 Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Thu, 24 Jul 2025 03:38:27 +0000 Subject: [PATCH 22/23] chore: v0.2.0 release --- Cargo.lock | 2 +- Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27651ae..3c99636 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3786,7 +3786,7 @@ checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" [[package]] name = "validator-voting" -version = "0.1.0" +version = "0.2.0" dependencies = [ "near-sdk", "near-workspaces", diff --git a/Cargo.toml b/Cargo.toml index bca4cb9..319fb99 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "validator-voting" description = "NEAR Validator Voting Contract" -version = "0.1.0" +version = "0.2.0" edition = "2021" # NEP-0330 is automatically implemented for all contracts built with https://github.com/near/cargo-near. From d7b63b97db0f85d6b0bb4a340daee81070bdec4f Mon Sep 17 00:00:00 2001 From: Linguists <95207870+linguists@users.noreply.github.com> Date: Thu, 24 Jul 2025 10:56:01 +0000 Subject: [PATCH 23/23] fix: allocate enough gas to callback function --- src/lib.rs | 3 +++ tests/test_vote.rs | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 2a60fa9..5f2bfdc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -97,6 +97,7 @@ impl Contract { pub fn vote(&mut self, vote: Vote, staking_pool_id: AccountId) -> Promise { ext_staking_pool::ext(staking_pool_id.clone()) .with_static_gas(GET_OWNER_ID_GAS) + .with_unused_gas_weight(0) .get_owner_id() .then(Self::ext(env::current_account_id()).on_get_pool_owner_id( env::predecessor_account_id(), @@ -460,6 +461,8 @@ mod tests { for i in 0..201 { // vote by each validator + let mut context = get_context(&voting_contract_id()); + set_context(&context); let voter = validator(i); vote(&mut contract, Vote::Yes, &voter); diff --git a/tests/test_vote.rs b/tests/test_vote.rs index 721eace..8e3cca3 100644 --- a/tests/test_vote.rs +++ b/tests/test_vote.rs @@ -130,6 +130,10 @@ async fn test_simple_vote() -> Result<(), Box> { "{:#?}", outcome.into_result().unwrap_err() ); + // print logs emitted by the contract + for log in outcome.logs() { + println!("contract log: {}", log); + } Ok(()) }