From dea3c21fc08388768eb4094d6c8254eedcf42245 Mon Sep 17 00:00:00 2001 From: sczembor Date: Thu, 20 Jul 2023 19:34:54 +0200 Subject: [PATCH 01/42] draft --- contracts/easy_poll/Cargo.toml | 10 +++++ contracts/easy_poll/README.md | 0 contracts/easy_poll/src/lib.rs | 64 ++++++++++++++++++++++++++++++ contracts/easy_poll/src/storage.rs | 47 ++++++++++++++++++++++ 4 files changed, 121 insertions(+) create mode 100644 contracts/easy_poll/Cargo.toml create mode 100644 contracts/easy_poll/README.md create mode 100644 contracts/easy_poll/src/lib.rs create mode 100644 contracts/easy_poll/src/storage.rs diff --git a/contracts/easy_poll/Cargo.toml b/contracts/easy_poll/Cargo.toml new file mode 100644 index 00000000..0b9b240d --- /dev/null +++ b/contracts/easy_poll/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "easy_poll" +version = "0.0.0" +authors = ["Stanislaw Czembor"] +edition = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +near-sdk.workspace = true diff --git a/contracts/easy_poll/README.md b/contracts/easy_poll/README.md new file mode 100644 index 00000000..e69de29b diff --git a/contracts/easy_poll/src/lib.rs b/contracts/easy_poll/src/lib.rs new file mode 100644 index 00000000..04f5c1ef --- /dev/null +++ b/contracts/easy_poll/src/lib.rs @@ -0,0 +1,64 @@ +struct Poll{ + verified_humans_only: bool, // required, if true only verified humans can vote, if false anyone can vote + questions: Vec, // required, a poll can have any number of questions + starts_at: usize, // required, time in milliseconds + end_at: usize, // required, time in milliseconds + title: String, // required + tags: Vec, // can be an empty vector + description: Option, // optional + link: Option, // optional + created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds + } + struct PollQuestion{ + question_type: PollQuestionType, // required + required: bool, // required, if true users can't vote without having an answer for this question + title: String, // required + description: Option, // optional + image: Option, // optional + labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question + choices: Option>, // if applicable, choices for the text and picture choices question + } + struct PollResult { + status: not_started | active | finished, + results: Vec<(usize, Vec)>, // question_id, list of answers + number_of_participants: u64, + } + + enum PollQuestionType { + YesNo, + TextChoices(min_choices, max_choices), + PictureChoices(min_choices, max_choices), + OpinionScale, + TextAnswer, + } + + // user can update the poll if starts_at > now + // it panics if + // - user tries to create an invalid poll + // - if poll aready exists and starts_at < now + fn create_poll(new_poll: Poll) -> PollId; + + struct Vote { + answers: Vec<(usize, PollQuestionAnswer)>, // question_id, answer + created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds + } + + enum PollQuestionAnswer { + YesNo(bool), + TextChoices(Vec), // should respect the min_choices, max_choices + PictureChoices(Vec), // should respect the min_choices, max_choices + OpinionScale(usize), // should be a number between 0 and 10 + TextAnswer(String), + } + + // user can change his vote when the poll is still active. + // it panics if + // - poll not found + // - poll not active + // - poll.verified_humans_only is true, and user is not verified on IAH + // - user tries to vote with an invalid answer to a question + fn vote(poll_id: PollId, vote: Vote); + + // returns None if poll is not found + fn result(poll_id: usize) -> Option; + \ No newline at end of file diff --git a/contracts/easy_poll/src/storage.rs b/contracts/easy_poll/src/storage.rs new file mode 100644 index 00000000..b53916c7 --- /dev/null +++ b/contracts/easy_poll/src/storage.rs @@ -0,0 +1,47 @@ +pub struct Poll{ + verified_humans_only: bool, // required, if true only verified humans can vote, if false anyone can vote + questions: Vec, // required, a poll can have any number of questions + starts_at: usize, // required, time in milliseconds + end_at: usize, // required, time in milliseconds + title: String, // required + tags: Vec, // can be an empty vector + description: Option, // optional + link: Option, // optional + created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds + }; +pub struct PollQuestion{ + question_type: PollQuestionType, // required + required: bool, // required, if true users can't vote without having an answer for this question + title: String, // required + description: Option, // optional + image: Option, // optional + labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question + choices: Option>, // if applicable, choices for the text and picture choices question + }; +pub struct PollResult { + status: not_started | active | finished, + results: Vec<(usize, Vec)>, // question_id, list of answers + number_of_participants: u64, + }; + + pub enum PollQuestionType { + YesNo, + TextChoices(min_choices, max_choices), + PictureChoices(min_choices, max_choices), + OpinionScale, + TextAnswer, + }; + + pub struct Vote { + answers: Vec<(usize, PollQuestionAnswer)>, // question_id, answer + created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds + }; + + pub enum PollQuestionAnswer { + YesNo(bool), + TextChoices(Vec), // should respect the min_choices, max_choices + PictureChoices(Vec), // should respect the min_choices, max_choices + OpinionScale(usize), // should be a number between 0 and 10 + TextAnswer(String), + }; + \ No newline at end of file From e8f1d3f4f513914f27e3b104dd738c499e35ccbc Mon Sep 17 00:00:00 2001 From: sczembor Date: Fri, 21 Jul 2023 15:09:16 +0200 Subject: [PATCH 02/42] draft --- contracts/Cargo.toml | 2 +- contracts/Makefile-common.mk | 2 +- contracts/easy-poll/Cargo.toml | 28 +++++ contracts/easy-poll/Makefile | 1 + contracts/easy-poll/README.md | 7 ++ contracts/easy-poll/src/lib.rs | 106 ++++++++++++++++++ .../{easy_poll => easy-poll}/src/storage.rs | 93 +++++++++------ contracts/easy_poll/Cargo.toml | 10 -- contracts/easy_poll/README.md | 0 contracts/easy_poll/src/lib.rs | 64 ----------- 10 files changed, 203 insertions(+), 110 deletions(-) create mode 100644 contracts/easy-poll/Cargo.toml create mode 100644 contracts/easy-poll/Makefile create mode 100644 contracts/easy-poll/README.md create mode 100644 contracts/easy-poll/src/lib.rs rename contracts/{easy_poll => easy-poll}/src/storage.rs (53%) delete mode 100644 contracts/easy_poll/Cargo.toml delete mode 100644 contracts/easy_poll/README.md delete mode 100644 contracts/easy_poll/src/lib.rs diff --git a/contracts/Cargo.toml b/contracts/Cargo.toml index b08a61af..3944e1b4 100644 --- a/contracts/Cargo.toml +++ b/contracts/Cargo.toml @@ -6,7 +6,7 @@ members = [ "oracle", "registry", "soulbound-class", - + "easy-poll", "human_checker", "ubi", "demo-issuer", diff --git a/contracts/Makefile-common.mk b/contracts/Makefile-common.mk index 78a95afe..bb20b601 100644 --- a/contracts/Makefile-common.mk +++ b/contracts/Makefile-common.mk @@ -1,7 +1,7 @@ build: @RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown --release @cp ../target/wasm32-unknown-unknown/release/*.wasm ../res/ - @cargo near abi + # @cargo near abi build-quick: @RUSTFLAGS='-C link-arg=-s' cargo build --target wasm32-unknown-unknown diff --git a/contracts/easy-poll/Cargo.toml b/contracts/easy-poll/Cargo.toml new file mode 100644 index 00000000..f83c3f7a --- /dev/null +++ b/contracts/easy-poll/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "easy-poll" +version = "0.0.0" +authors = [""] +edition = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[lib] +crate-type = ["cdylib", "rlib"] + + +[dependencies] +uint.workspace = true +near-sdk.workspace = true +serde_json.workspace = true + +cost = { path = "../cost" } +sbt = { path = "../sbt" } + +[dev-dependencies] +pretty_assertions.workspace = true +anyhow.workspace = true +tokio.workspace = true +workspaces.workspace = true +near-primitives.workspace = true +near-units.workspace = true +tracing.workspace = true diff --git a/contracts/easy-poll/Makefile b/contracts/easy-poll/Makefile new file mode 100644 index 00000000..c2b26eae --- /dev/null +++ b/contracts/easy-poll/Makefile @@ -0,0 +1 @@ +include ../Makefile-common.mk diff --git a/contracts/easy-poll/README.md b/contracts/easy-poll/README.md new file mode 100644 index 00000000..f9c08c7b --- /dev/null +++ b/contracts/easy-poll/README.md @@ -0,0 +1,7 @@ +# Proof of concept for Easy Poll + +Based on https://www.notion.so/near-ndc/EasyPoll-v2-f991a29781ca452db154c64922717d19#35d9a363be34495bb13ad5fa4b73cafe + +## Usage + +## Deployed contracts diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs new file mode 100644 index 00000000..9d891a32 --- /dev/null +++ b/contracts/easy-poll/src/lib.rs @@ -0,0 +1,106 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::UnorderedMap; +use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; + +pub use crate::storage::*; + +mod storage; + +#[near_bindgen] +#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] +pub struct Contract { + /// Account authorized to add new minting authority + pub admin: AccountId, + /// map of classId -> to set of accounts authorized to mint + pub polls: UnorderedMap, + /// SBT registry. + pub registry: AccountId, +} + +// Implement the contract structure +#[near_bindgen] +impl Contract { + /// @admin: account authorized to add new minting authority + /// @ttl: time to live for SBT expire. Must be number in miliseconds. + #[init] + pub fn new(admin: AccountId, registry: AccountId) -> Self { + Self { + admin, + polls: UnorderedMap::new(StorageKey::Polls), + registry, + } + } + + /********** + * QUERIES + **********/ + + // user can update the poll if starts_at > now + // it panics if + // - user tries to create an invalid poll + // - if poll aready exists and starts_at < now + pub fn create_poll(&self) -> PollId { + unimplemented!(); + } + + // user can change his vote when the poll is still active. + // it panics if + // - poll not found + // - poll not active + // - poll.verified_humans_only is true, and user is not verified on IAH + // - user tries to vote with an invalid answer to a question + pub fn vote(&self) { + unimplemented!(); + } + + // returns None if poll is not found + pub fn result(poll_id: usize) -> bool { + unimplemented!(); + } + /********** + * INTERNAL + **********/ + + fn assert_admin(&self) { + require!(self.admin == env::predecessor_account_id(), "not an admin"); + } +} + +#[cfg(test)] +mod tests { + use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, Balance, VMContext}; + use sbt::{ClassId, ContractMetadata, TokenMetadata}; + + use crate::Contract; + + fn alice() -> AccountId { + AccountId::new_unchecked("alice.near".to_string()) + } + + fn registry() -> AccountId { + AccountId::new_unchecked("registry.near".to_string()) + } + + fn admin() -> AccountId { + AccountId::new_unchecked("admin.near".to_string()) + } + + fn setup(predecessor: &AccountId) -> (VMContext, Contract) { + let mut ctx = VMContextBuilder::new() + .predecessor_account_id(admin()) + .block_timestamp(0) + .is_view(false) + .build(); + testing_env!(ctx.clone()); + let mut ctr = Contract::new(admin(), registry()); + ctx.predecessor_account_id = predecessor.clone(); + testing_env!(ctx.clone()); + return (ctx, ctr); + } + + #[test] + fn assert_admin() { + let (mut ctx, mut ctr) = setup(&admin()); + ctr.assert_admin(); + } +} diff --git a/contracts/easy_poll/src/storage.rs b/contracts/easy-poll/src/storage.rs similarity index 53% rename from contracts/easy_poll/src/storage.rs rename to contracts/easy-poll/src/storage.rs index b53916c7..7ad9dedb 100644 --- a/contracts/easy_poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -1,3 +1,35 @@ +use near_sdk::serde::{Deserialize, Serialize}; +use near_sdk::{AccountId, BorshStorageKey}; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; + +pub type PollId = u64; + +/// Helper structure for keys of the persistent collections. +#[derive(Deserialize, Serialize)] +#[serde(crate = "near_sdk::serde")] +pub enum PollQuestionAnswer { + YesNo(bool), + TextChoices(Vec), // should respect the min_choices, max_choices + PictureChoices(Vec), // should respect the min_choices, max_choices + OpinionScale(usize), // should be a number between 0 and 10 + TextAnswer(String), +} + +/// Helper structure for keys of the persistent collections. +#[derive(Deserialize, Serialize)] +#[serde(crate = "near_sdk::serde")] +pub struct PollQuestion{ + question_type: PollQuestionAnswer, // required + required: bool, // required, if true users can't vote without having an answer for this question + title: String, // required + description: Option, // optional + image: Option, // optional + labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question + choices: Option>, // if applicable, choices for the text and picture choices question +} + +#[derive(Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] pub struct Poll{ verified_humans_only: bool, // required, if true only verified humans can vote, if false anyone can vote questions: Vec, // required, a poll can have any number of questions @@ -8,40 +40,33 @@ pub struct Poll{ description: Option, // optional link: Option, // optional created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds - }; -pub struct PollQuestion{ - question_type: PollQuestionType, // required - required: bool, // required, if true users can't vote without having an answer for this question - title: String, // required - description: Option, // optional - image: Option, // optional - labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question - choices: Option>, // if applicable, choices for the text and picture choices question - }; -pub struct PollResult { - status: not_started | active | finished, - results: Vec<(usize, Vec)>, // question_id, list of answers - number_of_participants: u64, - }; +} - pub enum PollQuestionType { - YesNo, - TextChoices(min_choices, max_choices), - PictureChoices(min_choices, max_choices), - OpinionScale, - TextAnswer, - }; - - pub struct Vote { +#[derive(Deserialize, Serialize)] +#[serde(crate = "near_sdk::serde")] +pub struct Vote { answers: Vec<(usize, PollQuestionAnswer)>, // question_id, answer created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds - }; - - pub enum PollQuestionAnswer { - YesNo(bool), - TextChoices(Vec), // should respect the min_choices, max_choices - PictureChoices(Vec), // should respect the min_choices, max_choices - OpinionScale(usize), // should be a number between 0 and 10 - TextAnswer(String), - }; - \ No newline at end of file +} + +#[derive(Serialize)] +#[serde(crate = "near_sdk::serde")] + pub struct PollResult { + status: Status, + results: Vec<(usize, Vec)>, // question_id, list of answers + number_of_participants: u64, +} + +#[derive(Serialize)] +#[serde(crate = "near_sdk::serde")] +pub enum Status { + NotStarted, + Active, + Finished +} + +#[derive(BorshSerialize, BorshStorageKey)] +pub enum StorageKey { + Polls, + Another, +} \ No newline at end of file diff --git a/contracts/easy_poll/Cargo.toml b/contracts/easy_poll/Cargo.toml deleted file mode 100644 index 0b9b240d..00000000 --- a/contracts/easy_poll/Cargo.toml +++ /dev/null @@ -1,10 +0,0 @@ -[package] -name = "easy_poll" -version = "0.0.0" -authors = ["Stanislaw Czembor"] -edition = { workspace = true } -repository = { workspace = true } -license = { workspace = true } - -[dependencies] -near-sdk.workspace = true diff --git a/contracts/easy_poll/README.md b/contracts/easy_poll/README.md deleted file mode 100644 index e69de29b..00000000 diff --git a/contracts/easy_poll/src/lib.rs b/contracts/easy_poll/src/lib.rs deleted file mode 100644 index 04f5c1ef..00000000 --- a/contracts/easy_poll/src/lib.rs +++ /dev/null @@ -1,64 +0,0 @@ -struct Poll{ - verified_humans_only: bool, // required, if true only verified humans can vote, if false anyone can vote - questions: Vec, // required, a poll can have any number of questions - starts_at: usize, // required, time in milliseconds - end_at: usize, // required, time in milliseconds - title: String, // required - tags: Vec, // can be an empty vector - description: Option, // optional - link: Option, // optional - created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds - } - struct PollQuestion{ - question_type: PollQuestionType, // required - required: bool, // required, if true users can't vote without having an answer for this question - title: String, // required - description: Option, // optional - image: Option, // optional - labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question - choices: Option>, // if applicable, choices for the text and picture choices question - } - struct PollResult { - status: not_started | active | finished, - results: Vec<(usize, Vec)>, // question_id, list of answers - number_of_participants: u64, - } - - enum PollQuestionType { - YesNo, - TextChoices(min_choices, max_choices), - PictureChoices(min_choices, max_choices), - OpinionScale, - TextAnswer, - } - - // user can update the poll if starts_at > now - // it panics if - // - user tries to create an invalid poll - // - if poll aready exists and starts_at < now - fn create_poll(new_poll: Poll) -> PollId; - - struct Vote { - answers: Vec<(usize, PollQuestionAnswer)>, // question_id, answer - created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds - } - - enum PollQuestionAnswer { - YesNo(bool), - TextChoices(Vec), // should respect the min_choices, max_choices - PictureChoices(Vec), // should respect the min_choices, max_choices - OpinionScale(usize), // should be a number between 0 and 10 - TextAnswer(String), - } - - // user can change his vote when the poll is still active. - // it panics if - // - poll not found - // - poll not active - // - poll.verified_humans_only is true, and user is not verified on IAH - // - user tries to vote with an invalid answer to a question - fn vote(poll_id: PollId, vote: Vote); - - // returns None if poll is not found - fn result(poll_id: usize) -> Option; - \ No newline at end of file From a7b66d66565637704b8831079f1bbe9bfbd37686 Mon Sep 17 00:00:00 2001 From: sczembor Date: Tue, 25 Jul 2023 11:01:30 +0200 Subject: [PATCH 03/42] draft --- contracts/easy-poll/src/lib.rs | 109 +++++++++++++++++++++++++++-- contracts/easy-poll/src/storage.rs | 87 ++++++++++++++--------- 2 files changed, 157 insertions(+), 39 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 9d891a32..902a1462 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -1,3 +1,6 @@ +use std::fmt::format; +use std::future::Future; + use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::UnorderedMap; use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; @@ -12,9 +15,13 @@ pub struct Contract { /// Account authorized to add new minting authority pub admin: AccountId, /// map of classId -> to set of accounts authorized to mint - pub polls: UnorderedMap, + pub polls: UnorderedMap, + pub results: UnorderedMap>, + pub poll_questions_results: UnorderedMap>, /// SBT registry. pub registry: AccountId, + /// next poll id + pub next_poll_id: PollId, } // Implement the contract structure @@ -27,7 +34,10 @@ impl Contract { Self { admin, polls: UnorderedMap::new(StorageKey::Polls), + results: UnorderedMap::new(StorageKey::Results), + poll_questions_results: UnorderedMap::new(StorageKey::Answers), registry, + next_poll_id: 0, } } @@ -39,28 +49,115 @@ impl Contract { // it panics if // - user tries to create an invalid poll // - if poll aready exists and starts_at < now - pub fn create_poll(&self) -> PollId { - unimplemented!(); + pub fn create_poll( + &mut self, + iah_only: bool, + questions: Vec, + starts_at: u64, + ends_at: u64, + title: String, + tags: Vec, + description: Option, + link: Option, + ) -> PollId { + let created_at = env::block_timestamp_ms(); + require!( + created_at < starts_at, + format!("poll start must be in the future") + ); + let poll_id = self.next_poll_id; + self.next_poll_id += 1; + self.polls.insert( + &poll_id, + &Poll { + iah_only, + questions, + starts_at, + ends_at, + title, + tags, + description, + link, + created_at, + }, + ); + poll_id } - // user can change his vote when the poll is still active. + // user can change his answer when the poll is still active. // it panics if // - poll not found // - poll not active // - poll.verified_humans_only is true, and user is not verified on IAH // - user tries to vote with an invalid answer to a question - pub fn vote(&self) { + pub fn respond(&mut self, poll_id: PollId, answers: Vec>) { + // check if poll exists and is active + self.assert_active(poll_id); + // if iah calls the registry to verify the iah sbt + self.assert_human(poll_id); + let questions = self.polls.get(&poll_id).unwrap().questions; + let unwrapped_answers = Vec::new(); + for i in 0..questions.len() { + match answers[i] { + Some(PollQuestionAnswer::YesNo(value)) => { + if value { + let results = self.poll_questions_results.get((&poll_id, i)); + } + } + PollQuestionAnswer::TextChoices => { + // TODO: implement + } + PollQuestionAnswer::PictureChoices(value) => { + // TODO: implement + } + PollQuestionAnswer::OpinionScale(value) => { + // TODO: implement + } + PollQuestionAnswer::TextAnswer => { + println!("Not supported yet"); + } + } + + require!( + questions[i].required && answers[i].is_some(), + format!("poll question {} requires an answer", i) + ); + if answers[i].is_some() { + unwrapped_answers.push(answers[i].unwrap()); + } + } + let results = self.results.get(&poll_id).unwrap(); + results.append(unwrapped_answers); + self.results.insert(poll_id, results); + + // update the results + } + + pub fn my_responder(&self, poll_id: PollId) -> Vec> { unimplemented!(); } // returns None if poll is not found - pub fn result(poll_id: usize) -> bool { + pub fn result(poll_id: usize) -> PollResults { unimplemented!(); } + /********** * INTERNAL **********/ + fn assert_active(&self, poll_id: PollId) { + let poll = self.polls.get(&poll_id).expect("poll not found"); + require!(poll.ends_at > env::block_timestamp_ms(), "poll not active"); + } + + fn assert_human(&self, poll_id: PollId) { + let poll = self.polls.get(&poll_id).unwrap(); + if poll.iah_only { + // TODO: call registry to verify humanity + } + } + fn assert_admin(&self) { require!(self.admin == env::predecessor_account_id(), "not an admin"); } diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 7ad9dedb..63823ba7 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -1,58 +1,78 @@ +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; use near_sdk::{AccountId, BorshStorageKey}; -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; pub type PollId = u64; /// Helper structure for keys of the persistent collections. -#[derive(Deserialize, Serialize)] +#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize)] #[serde(crate = "near_sdk::serde")] pub enum PollQuestionAnswer { YesNo(bool), - TextChoices(Vec), // should respect the min_choices, max_choices - PictureChoices(Vec), // should respect the min_choices, max_choices - OpinionScale(usize), // should be a number between 0 and 10 + TextChoices(Vec), // should respect the min_choices, max_choices + PictureChoices(Vec), // should respect the min_choices, max_choices + OpinionScale(u64), // should be a number between 0 and 10 TextAnswer(String), } +pub enum PollQuestionResult { + YesNo((u32, u32)), + TextChoices(Vec), // should respect the min_choices, max_choices + PictureChoices(Vec), // should respect the min_choices, max_choices + OpinionScale(OpinionScaleResult), // mean value +} + +pub struct OpinionScaleResult { + pub sum: u32, + pub num: u32, +} + /// Helper structure for keys of the persistent collections. -#[derive(Deserialize, Serialize)] +#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize)] #[serde(crate = "near_sdk::serde")] -pub struct PollQuestion{ - question_type: PollQuestionAnswer, // required - required: bool, // required, if true users can't vote without having an answer for this question - title: String, // required - description: Option, // optional - image: Option, // optional - labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question - choices: Option>, // if applicable, choices for the text and picture choices question +pub struct PollQuestion { + pub question_type: PollQuestionAnswer, // required + pub required: bool, // required, if true users can't vote without having an answer for this question + pub title: String, // required + pub description: Option, // optional + pub image: Option, // optional + pub labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question + pub choices: Option>, // if applicable, choices for the text and picture choices question } -#[derive(Serialize, Deserialize)] +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[serde(crate = "near_sdk::serde")] -pub struct Poll{ - verified_humans_only: bool, // required, if true only verified humans can vote, if false anyone can vote - questions: Vec, // required, a poll can have any number of questions - starts_at: usize, // required, time in milliseconds - end_at: usize, // required, time in milliseconds - title: String, // required - tags: Vec, // can be an empty vector - description: Option, // optional - link: Option, // optional - created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds +pub struct Poll { + pub iah_only: bool, // required, if true only verified humans can vote, if false anyone can vote + pub questions: Vec, // required, a poll can have any number of questions + pub starts_at: u64, // required, time in milliseconds + pub ends_at: u64, // required, time in milliseconds + pub title: String, // required + pub tags: Vec, // can be an empty vector + pub description: Option, // optional + pub link: Option, // optional + pub created_at: u64, // should be assigned by the smart contract not the user, time in milliseconds } - + #[derive(Deserialize, Serialize)] #[serde(crate = "near_sdk::serde")] -pub struct Vote { +pub struct PollResponse { answers: Vec<(usize, PollQuestionAnswer)>, // question_id, answer - created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds + created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds +} + +#[derive(Deserialize, Serialize)] +#[serde(crate = "near_sdk::serde")] +pub struct PollResults { + pub status: Status, + pub number_of_participants: u64, + pub answers: Vec<(usize, PollQuestionResult)>, // question_id, answer } #[derive(Serialize)] #[serde(crate = "near_sdk::serde")] - pub struct PollResult { - status: Status, +pub struct PollResult { + status: Status, results: Vec<(usize, Vec)>, // question_id, list of answers number_of_participants: u64, } @@ -62,11 +82,12 @@ pub struct Vote { pub enum Status { NotStarted, Active, - Finished + Finished, } #[derive(BorshSerialize, BorshStorageKey)] pub enum StorageKey { Polls, - Another, -} \ No newline at end of file + Results, + Answers, +} From afe26d2ef13dc7d4b939cc397646ebe7a9083037 Mon Sep 17 00:00:00 2001 From: sczembor Date: Mon, 31 Jul 2023 17:19:05 +0200 Subject: [PATCH 04/42] finish respond method; add simple unit test --- contracts/easy-poll/src/lib.rs | 139 ++++++++++++++++++++--------- contracts/easy-poll/src/storage.rs | 41 +++++---- 2 files changed, 117 insertions(+), 63 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 902a1462..1eaf8fc9 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -16,8 +16,8 @@ pub struct Contract { pub admin: AccountId, /// map of classId -> to set of accounts authorized to mint pub polls: UnorderedMap, - pub results: UnorderedMap>, - pub poll_questions_results: UnorderedMap>, + pub results: UnorderedMap, + pub answers: UnorderedMap<(PollId, AccountId), Vec>, /// SBT registry. pub registry: AccountId, /// next poll id @@ -35,7 +35,7 @@ impl Contract { admin, polls: UnorderedMap::new(StorageKey::Polls), results: UnorderedMap::new(StorageKey::Results), - poll_questions_results: UnorderedMap::new(StorageKey::Answers), + answers: UnorderedMap::new(StorageKey::Answers), registry, next_poll_id: 0, } @@ -45,6 +45,18 @@ impl Contract { * QUERIES **********/ + pub fn my_respond(&self, poll_id: PollId) -> Vec { + let caller = env::predecessor_account_id(); + self.answers + .get(&(poll_id, caller)) + .expect("respond not found") + } + + // returns None if poll is not found + pub fn result(&self, poll_id: usize) -> Results { + unimplemented!(); + } + // user can update the poll if starts_at > now // it panics if // - user tries to create an invalid poll @@ -52,7 +64,7 @@ impl Contract { pub fn create_poll( &mut self, iah_only: bool, - questions: Vec, + questions: Vec, starts_at: u64, ends_at: u64, title: String, @@ -90,68 +102,78 @@ impl Contract { // - poll not active // - poll.verified_humans_only is true, and user is not verified on IAH // - user tries to vote with an invalid answer to a question - pub fn respond(&mut self, poll_id: PollId, answers: Vec>) { + pub fn respond(&mut self, poll_id: PollId, answers: Vec>) { + let caller = env::predecessor_account_id(); // check if poll exists and is active self.assert_active(poll_id); // if iah calls the registry to verify the iah sbt - self.assert_human(poll_id); - let questions = self.polls.get(&poll_id).unwrap().questions; - let unwrapped_answers = Vec::new(); + self.assert_human(poll_id, &caller); + let questions: Vec = self.polls.get(&poll_id).unwrap().questions; + let mut unwrapped_answers: Vec = Vec::new(); + let poll_results = self.results.get(&poll_id).unwrap(); + let mut results = poll_results.results; for i in 0..questions.len() { - match answers[i] { - Some(PollQuestionAnswer::YesNo(value)) => { - if value { - let results = self.poll_questions_results.get((&poll_id, i)); + require!( + questions[i].required && answers[i].is_some(), + format!("poll question {} requires an answer", i) + ); + + match (&answers[i], &results[i]) { + (Some(Answer::YesNo(yes_no)), Result::YesNo((yes, no))) => { + if *yes_no { + results[i] = Result::YesNo((*yes + 1, *no)); + } else { + results[i] = Result::YesNo((*yes + 1, *no + 1)); } } - PollQuestionAnswer::TextChoices => { - // TODO: implement - } - PollQuestionAnswer::PictureChoices(value) => { - // TODO: implement + (Some(Answer::TextChoices(value)), Result::TextChoices(vector)) => { + let mut new_vec = Vec::new(); + for i in value { + new_vec[*i] = vector[*i] + *i as u32; + } + results[i] = Result::TextChoices(new_vec); } - PollQuestionAnswer::OpinionScale(value) => { - // TODO: implement + (Some(Answer::PictureChoices(value)), Result::PictureChoices(vector)) => { + let mut new_vec = Vec::new(); + for i in value { + new_vec[*i] = vector[*i] + *i as u32; + } + results[i] = Result::PictureChoices(new_vec); } - PollQuestionAnswer::TextAnswer => { - println!("Not supported yet"); + (Some(Answer::OpinionScale(value)), Result::OpinionScale(opinion)) => { + results.insert( + i, + Result::OpinionScale(OpinionScaleResult { + sum: opinion.sum + *value as u32, + num: opinion.num + 1 as u32, + }), + ); } + // (Some(Answer::TextAnswer(answer)), Result::TextAnswer(answers)) => { + // let mut new_vec = Vec::new(); + // new_vec = *answers; + // new_vec.push(*answer); + // } + (_, _) => env::panic_str("error"), } - - require!( - questions[i].required && answers[i].is_some(), - format!("poll question {} requires an answer", i) - ); if answers[i].is_some() { - unwrapped_answers.push(answers[i].unwrap()); + unwrapped_answers.push(answers[i].clone().unwrap()); } } - let results = self.results.get(&poll_id).unwrap(); - results.append(unwrapped_answers); - self.results.insert(poll_id, results); - - // update the results - } - - pub fn my_responder(&self, poll_id: PollId) -> Vec> { - unimplemented!(); - } - - // returns None if poll is not found - pub fn result(poll_id: usize) -> PollResults { - unimplemented!(); + let mut answers = self.answers.get(&(poll_id, caller.clone())).unwrap(); + answers.append(&mut unwrapped_answers); + self.answers.insert(&(poll_id, caller), &answers); } /********** * INTERNAL **********/ - fn assert_active(&self, poll_id: PollId) { let poll = self.polls.get(&poll_id).expect("poll not found"); require!(poll.ends_at > env::block_timestamp_ms(), "poll not active"); } - fn assert_human(&self, poll_id: PollId) { + fn assert_human(&self, poll_id: PollId, caller: &AccountId) { let poll = self.polls.get(&poll_id).unwrap(); if poll.iah_only { // TODO: call registry to verify humanity @@ -168,7 +190,7 @@ mod tests { use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, Balance, VMContext}; use sbt::{ClassId, ContractMetadata, TokenMetadata}; - use crate::Contract; + use crate::{Answer, Contract, Question}; fn alice() -> AccountId { AccountId::new_unchecked("alice.near".to_string()) @@ -200,4 +222,33 @@ mod tests { let (mut ctx, mut ctr) = setup(&admin()); ctr.assert_admin(); } + + #[test] + fn flow1() { + let question = Question { + question_type: Answer::YesNo(true), // required + required: true, // required, if true users can't vote without having an answer for this question + title: String::from("Hello, world!"), // required + description: None, // optional + image: None, // optional + labels: None, // if applicable, labels for the opinion scale question + choices: None, // if applicable, choices for the text and picture choices question + }; + let tags = vec![String::from("tag1"), String::from("tag2")]; + let (mut ctx, mut ctr) = setup(&admin()); + let poll_id = ctr.create_poll( + false, + vec![question], + 1, + 100, + String::from("Hello, world!"), + tags, + None, + None, + ); + ctx.predecessor_account_id = alice(); + testing_env!(ctx.clone()); + let answers = vec![Some(Answer::YesNo(true))]; + ctr.respond(poll_id, answers); + } } diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 63823ba7..9c6b0e67 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -5,23 +5,26 @@ use near_sdk::{AccountId, BorshStorageKey}; pub type PollId = u64; /// Helper structure for keys of the persistent collections. -#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize)] +#[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] #[serde(crate = "near_sdk::serde")] -pub enum PollQuestionAnswer { +pub enum Answer { YesNo(bool), TextChoices(Vec), // should respect the min_choices, max_choices PictureChoices(Vec), // should respect the min_choices, max_choices OpinionScale(u64), // should be a number between 0 and 10 TextAnswer(String), } - -pub enum PollQuestionResult { - YesNo((u32, u32)), - TextChoices(Vec), // should respect the min_choices, max_choices - PictureChoices(Vec), // should respect the min_choices, max_choices +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] +pub enum Result { + YesNo((u32, u32)), // yes, no + TextChoices(Vec), // should respect the min_choices, max_choices + PictureChoices(Vec), // should respect the min_choices, max_choices OpinionScale(OpinionScaleResult), // mean value + TextAnswer(Vec), } - +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[serde(crate = "near_sdk::serde")] pub struct OpinionScaleResult { pub sum: u32, pub num: u32, @@ -30,8 +33,8 @@ pub struct OpinionScaleResult { /// Helper structure for keys of the persistent collections. #[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize)] #[serde(crate = "near_sdk::serde")] -pub struct PollQuestion { - pub question_type: PollQuestionAnswer, // required +pub struct Question { + pub question_type: Answer, // required pub required: bool, // required, if true users can't vote without having an answer for this question pub title: String, // required pub description: Option, // optional @@ -44,7 +47,7 @@ pub struct PollQuestion { #[serde(crate = "near_sdk::serde")] pub struct Poll { pub iah_only: bool, // required, if true only verified humans can vote, if false anyone can vote - pub questions: Vec, // required, a poll can have any number of questions + pub questions: Vec, // required, a poll can have any number of questions pub starts_at: u64, // required, time in milliseconds pub ends_at: u64, // required, time in milliseconds pub title: String, // required @@ -57,27 +60,27 @@ pub struct Poll { #[derive(Deserialize, Serialize)] #[serde(crate = "near_sdk::serde")] pub struct PollResponse { - answers: Vec<(usize, PollQuestionAnswer)>, // question_id, answer + answers: Vec<(usize, Answer)>, // question_id, answer created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds } -#[derive(Deserialize, Serialize)] +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[serde(crate = "near_sdk::serde")] -pub struct PollResults { +pub struct Results { pub status: Status, pub number_of_participants: u64, - pub answers: Vec<(usize, PollQuestionResult)>, // question_id, answer + pub results: Vec, // question_id, result (sum of yes etc.) } -#[derive(Serialize)] +#[derive(Serialize, Clone)] #[serde(crate = "near_sdk::serde")] -pub struct PollResult { +pub struct Answers { status: Status, - results: Vec<(usize, Vec)>, // question_id, list of answers number_of_participants: u64, + answers: Vec<(usize, Vec)>, // question_id, list of answers } -#[derive(Serialize)] +#[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] #[serde(crate = "near_sdk::serde")] pub enum Status { NotStarted, From d4a35d8533f9aca864f23650b5247ffcbf247322 Mon Sep 17 00:00:00 2001 From: sczembor Date: Wed, 2 Aug 2023 12:29:55 +0200 Subject: [PATCH 05/42] add custom errors --- contracts/easy-poll/src/errors.rs | 25 +++++++++++++ contracts/easy-poll/src/lib.rs | 60 ++++++++++++++++++++++-------- contracts/easy-poll/src/storage.rs | 8 ++-- 3 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 contracts/easy-poll/src/errors.rs diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs new file mode 100644 index 00000000..d3cdc752 --- /dev/null +++ b/contracts/easy-poll/src/errors.rs @@ -0,0 +1,25 @@ +use near_sdk::env::panic_str; +use near_sdk::FunctionError; + +/// Contract errors +#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq))] +#[derive(Debug)] +pub enum PollError { + RequiredAnswer, + NoSBTs, + NotFound, + NotActive, +} + +impl FunctionError for PollError { + fn panic(&self) -> ! { + match self { + PollError::RequiredAnswer => { + panic_str("Answer to a required question was not provided") + } + PollError::NoSBTs => panic_str("voter is not a verified human"), + PollError::NotFound => panic_str("poll not found"), + PollError::NotActive => panic_str("poll is not active"), + } + } +} diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 1eaf8fc9..eba0190f 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -2,11 +2,13 @@ use std::fmt::format; use std::future::Future; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::collections::UnorderedMap; +use near_sdk::collections::{LookupMap, UnorderedMap}; use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; +pub use crate::errors::PollError; pub use crate::storage::*; +mod errors; mod storage; #[near_bindgen] @@ -16,7 +18,7 @@ pub struct Contract { pub admin: AccountId, /// map of classId -> to set of accounts authorized to mint pub polls: UnorderedMap, - pub results: UnorderedMap, + pub results: LookupMap, pub answers: UnorderedMap<(PollId, AccountId), Vec>, /// SBT registry. pub registry: AccountId, @@ -34,7 +36,7 @@ impl Contract { Self { admin, polls: UnorderedMap::new(StorageKey::Polls), - results: UnorderedMap::new(StorageKey::Results), + results: LookupMap::new(StorageKey::Results), answers: UnorderedMap::new(StorageKey::Answers), registry, next_poll_id: 0, @@ -57,6 +59,19 @@ impl Contract { unimplemented!(); } + // TODO: limit the max lenght of single answer and based on that return a fixed value of answers + // Function must be called until true is returned -> meaning all the answers were returned + // returns None if poll is not found + // `question` must be an index of the text question in the poll + pub fn result_answers( + &self, + poll_id: usize, + question: usize, + from_answer: u64, + ) -> (Vec, bool) { + //TODO check if question is type `TextAnswer` + unimplemented!(); + } // user can update the poll if starts_at > now // it panics if // - user tries to create an invalid poll @@ -93,6 +108,12 @@ impl Contract { created_at, }, ); + let poll_results = Results { + status: Status::Active, + number_of_participants: 0, + results: vec![PollResult::YesNo((0, 0))], + }; + self.results.insert(&poll_id, &poll_results); poll_id } @@ -102,7 +123,12 @@ impl Contract { // - poll not active // - poll.verified_humans_only is true, and user is not verified on IAH // - user tries to vote with an invalid answer to a question - pub fn respond(&mut self, poll_id: PollId, answers: Vec>) { + #[handle_result] + pub fn respond( + &mut self, + poll_id: PollId, + answers: Vec>, + ) -> Result<(), PollError> { let caller = env::predecessor_account_id(); // check if poll exists and is active self.assert_active(poll_id); @@ -119,31 +145,31 @@ impl Contract { ); match (&answers[i], &results[i]) { - (Some(Answer::YesNo(yes_no)), Result::YesNo((yes, no))) => { + (Some(Answer::YesNo(yes_no)), PollResult::YesNo((yes, no))) => { if *yes_no { - results[i] = Result::YesNo((*yes + 1, *no)); + results[i] = PollResult::YesNo((*yes + 1, *no)); } else { - results[i] = Result::YesNo((*yes + 1, *no + 1)); + results[i] = PollResult::YesNo((*yes + 1, *no + 1)); } } - (Some(Answer::TextChoices(value)), Result::TextChoices(vector)) => { + (Some(Answer::TextChoices(value)), PollResult::TextChoices(vector)) => { let mut new_vec = Vec::new(); for i in value { new_vec[*i] = vector[*i] + *i as u32; } - results[i] = Result::TextChoices(new_vec); + results[i] = PollResult::TextChoices(new_vec); } - (Some(Answer::PictureChoices(value)), Result::PictureChoices(vector)) => { + (Some(Answer::PictureChoices(value)), PollResult::PictureChoices(vector)) => { let mut new_vec = Vec::new(); for i in value { new_vec[*i] = vector[*i] + *i as u32; } - results[i] = Result::PictureChoices(new_vec); + results[i] = PollResult::PictureChoices(new_vec); } - (Some(Answer::OpinionScale(value)), Result::OpinionScale(opinion)) => { + (Some(Answer::OpinionScale(value)), PollResult::OpinionScale(opinion)) => { results.insert( i, - Result::OpinionScale(OpinionScaleResult { + PollResult::OpinionScale(OpinionScaleResult { sum: opinion.sum + *value as u32, num: opinion.num + 1 as u32, }), @@ -160,9 +186,13 @@ impl Contract { unwrapped_answers.push(answers[i].clone().unwrap()); } } - let mut answers = self.answers.get(&(poll_id, caller.clone())).unwrap(); + let mut answers = self + .answers + .get(&(poll_id, caller.clone())) + .unwrap_or(Vec::new()); answers.append(&mut unwrapped_answers); self.answers.insert(&(poll_id, caller), &answers); + Ok(()) } /********** @@ -232,7 +262,7 @@ mod tests { description: None, // optional image: None, // optional labels: None, // if applicable, labels for the opinion scale question - choices: None, // if applicable, choices for the text and picture choices question + // choices: None, // if applicable, choices for the text and picture choices question }; let tags = vec![String::from("tag1"), String::from("tag2")]; let (mut ctx, mut ctr) = setup(&admin()); diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 9c6b0e67..8be96770 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -16,7 +16,7 @@ pub enum Answer { } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[serde(crate = "near_sdk::serde")] -pub enum Result { +pub enum PollResult { YesNo((u32, u32)), // yes, no TextChoices(Vec), // should respect the min_choices, max_choices PictureChoices(Vec), // should respect the min_choices, max_choices @@ -34,13 +34,13 @@ pub struct OpinionScaleResult { #[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize)] #[serde(crate = "near_sdk::serde")] pub struct Question { - pub question_type: Answer, // required + pub question_type: Answer, // required pub required: bool, // required, if true users can't vote without having an answer for this question pub title: String, // required pub description: Option, // optional pub image: Option, // optional pub labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question - pub choices: Option>, // if applicable, choices for the text and picture choices question + // pub choices: Option>, // if applicable, choices for the text and picture choices question TODO: make sure we dont need it } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] @@ -69,7 +69,7 @@ pub struct PollResponse { pub struct Results { pub status: Status, pub number_of_participants: u64, - pub results: Vec, // question_id, result (sum of yes etc.) + pub results: Vec, // question_id, result (sum of yes etc.) } #[derive(Serialize, Clone)] From f74f1239e8842e9f15de62ecc699add429b443cf Mon Sep 17 00:00:00 2001 From: sczembor Date: Wed, 2 Aug 2023 18:31:20 +0200 Subject: [PATCH 06/42] add unit tests for responds --- contracts/easy-poll/src/lib.rs | 227 +++++++++++++++++++++++------ contracts/easy-poll/src/storage.rs | 8 +- 2 files changed, 189 insertions(+), 46 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index eba0190f..5b3049d6 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -55,8 +55,8 @@ impl Contract { } // returns None if poll is not found - pub fn result(&self, poll_id: usize) -> Results { - unimplemented!(); + pub fn results(&self, poll_id: u64) -> Results { + self.results.get(&poll_id).expect("poll not found") } // TODO: limit the max lenght of single answer and based on that return a fixed value of answers @@ -94,6 +94,7 @@ impl Contract { ); let poll_id = self.next_poll_id; self.next_poll_id += 1; + self.initalize_results(poll_id, &questions); self.polls.insert( &poll_id, &Poll { @@ -108,12 +109,6 @@ impl Contract { created_at, }, ); - let poll_results = Results { - status: Status::Active, - number_of_participants: 0, - results: vec![PollResult::YesNo((0, 0))], - }; - self.results.insert(&poll_id, &poll_results); poll_id } @@ -132,24 +127,24 @@ impl Contract { let caller = env::predecessor_account_id(); // check if poll exists and is active self.assert_active(poll_id); + self.assert_answered(poll_id, &caller); // if iah calls the registry to verify the iah sbt self.assert_human(poll_id, &caller); let questions: Vec = self.polls.get(&poll_id).unwrap().questions; let mut unwrapped_answers: Vec = Vec::new(); - let poll_results = self.results.get(&poll_id).unwrap(); - let mut results = poll_results.results; + let mut poll_results = self.results.get(&poll_id).unwrap(); + // let mut results = poll_results.results; for i in 0..questions.len() { - require!( - questions[i].required && answers[i].is_some(), - format!("poll question {} requires an answer", i) - ); + if questions[i].required && answers[i].is_none() { + env::panic_str(format!("poll question {} requires an answer", i).as_str()); + } - match (&answers[i], &results[i]) { + match (&answers[i], &poll_results.results[i]) { (Some(Answer::YesNo(yes_no)), PollResult::YesNo((yes, no))) => { if *yes_no { - results[i] = PollResult::YesNo((*yes + 1, *no)); + poll_results.results[i] = PollResult::YesNo((*yes + 1, *no)); } else { - results[i] = PollResult::YesNo((*yes + 1, *no + 1)); + poll_results.results[i] = PollResult::YesNo((*yes, *no + 1)); } } (Some(Answer::TextChoices(value)), PollResult::TextChoices(vector)) => { @@ -157,30 +152,30 @@ impl Contract { for i in value { new_vec[*i] = vector[*i] + *i as u32; } - results[i] = PollResult::TextChoices(new_vec); + poll_results.results[i] = PollResult::TextChoices(new_vec); } (Some(Answer::PictureChoices(value)), PollResult::PictureChoices(vector)) => { let mut new_vec = Vec::new(); for i in value { new_vec[*i] = vector[*i] + *i as u32; } - results[i] = PollResult::PictureChoices(new_vec); + poll_results.results[i] = PollResult::PictureChoices(new_vec); } (Some(Answer::OpinionScale(value)), PollResult::OpinionScale(opinion)) => { - results.insert( - i, - PollResult::OpinionScale(OpinionScaleResult { - sum: opinion.sum + *value as u32, - num: opinion.num + 1 as u32, - }), - ); + if *value > 10 { + env::panic_str("opinion must be between 0 and 10"); + } + poll_results.results[i] = PollResult::OpinionScale(OpinionScaleResult { + sum: opinion.sum + *value as u32, + num: opinion.num + 1 as u32, + }); } // (Some(Answer::TextAnswer(answer)), Result::TextAnswer(answers)) => { // let mut new_vec = Vec::new(); // new_vec = *answers; // new_vec.push(*answer); // } - (_, _) => env::panic_str("error"), + (_, _) => (), } if answers[i].is_some() { unwrapped_answers.push(answers[i].clone().unwrap()); @@ -192,6 +187,10 @@ impl Contract { .unwrap_or(Vec::new()); answers.append(&mut unwrapped_answers); self.answers.insert(&(poll_id, caller), &answers); + // update the status and number of participants + poll_results.status = Status::Active; + poll_results.number_of_participants += 1; + self.results.insert(&poll_id, &poll_results); Ok(()) } @@ -200,7 +199,15 @@ impl Contract { **********/ fn assert_active(&self, poll_id: PollId) { let poll = self.polls.get(&poll_id).expect("poll not found"); - require!(poll.ends_at > env::block_timestamp_ms(), "poll not active"); + let current_timestamp = env::block_timestamp_ms(); + require!( + poll.starts_at < current_timestamp, + format!( + "poll have not started yet, start_at: {:?}, current_timestamp: {:?}", + poll.starts_at, current_timestamp + ) + ); + require!(poll.ends_at > current_timestamp, "poll not active"); } fn assert_human(&self, poll_id: PollId, caller: &AccountId) { @@ -213,6 +220,36 @@ impl Contract { fn assert_admin(&self) { require!(self.admin == env::predecessor_account_id(), "not an admin"); } + + fn assert_answered(&self, poll_id: PollId, caller: &AccountId) { + require!( + self.answers.get(&(poll_id, caller.clone())).is_none(), + format!("user: {} has already answered", caller) + ); + } + + fn initalize_results(&mut self, poll_id: PollId, questions: &Vec) { + let mut results = Vec::new(); + for question in questions { + results.push(match question.question_type { + Answer::YesNo(_) => PollResult::YesNo((0, 0)), + Answer::TextChoices(_) => PollResult::TextChoices(Vec::new()), + Answer::PictureChoices(_) => PollResult::PictureChoices(Vec::new()), + Answer::OpinionScale(_) => { + PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }) + } + Answer::TextAnswer(_) => PollResult::TextAnswer(Vec::new()), + }); + } + self.results.insert( + &poll_id, + &Results { + status: Status::NotStarted, + number_of_participants: 0, + results: results, + }, + ); + } } #[cfg(test)] @@ -220,12 +257,22 @@ mod tests { use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, Balance, VMContext}; use sbt::{ClassId, ContractMetadata, TokenMetadata}; - use crate::{Answer, Contract, Question}; + use crate::{Answer, Contract, OpinionScaleResult, PollResult, Question, Results, Status}; + + const MILI_SECOND: u64 = 1000000; // nanoseconds fn alice() -> AccountId { AccountId::new_unchecked("alice.near".to_string()) } + fn bob() -> AccountId { + AccountId::new_unchecked("bob.near".to_string()) + } + + fn charlie() -> AccountId { + AccountId::new_unchecked("charlie.near".to_string()) + } + fn registry() -> AccountId { AccountId::new_unchecked("registry.near".to_string()) } @@ -234,10 +281,46 @@ mod tests { AccountId::new_unchecked("admin.near".to_string()) } + fn questions() -> Vec { + let mut questions = Vec::new(); + questions.push(Question { + question_type: Answer::YesNo(true), + required: false, + title: String::from("Yes and no test!"), + description: None, + image: None, + labels: None, + choices: None, + }); + questions.push(Question { + question_type: Answer::TextChoices(vec![0, 0, 0]), + required: false, + title: String::from("Yes and no test!"), + description: None, + image: None, + labels: None, + choices: Some(vec![ + String::from("agree"), + String::from("disagree"), + String::from("no opinion"), + ]), + }); + questions.push(Question { + question_type: Answer::OpinionScale(0), + required: false, + title: String::from("Opinion test!"), + description: None, + image: None, + labels: None, + choices: None, + }); + questions + } + fn setup(predecessor: &AccountId) -> (VMContext, Contract) { let mut ctx = VMContextBuilder::new() .predecessor_account_id(admin()) - .block_timestamp(0) + .block_timestamp(MILI_SECOND) .is_view(false) .build(); testing_env!(ctx.clone()); @@ -254,22 +337,13 @@ mod tests { } #[test] - fn flow1() { - let question = Question { - question_type: Answer::YesNo(true), // required - required: true, // required, if true users can't vote without having an answer for this question - title: String::from("Hello, world!"), // required - description: None, // optional - image: None, // optional - labels: None, // if applicable, labels for the opinion scale question - // choices: None, // if applicable, choices for the text and picture choices question - }; + fn yes_no_flow() { let tags = vec![String::from("tag1"), String::from("tag2")]; let (mut ctx, mut ctr) = setup(&admin()); let poll_id = ctr.create_poll( false, - vec![question], - 1, + questions(), + 2, 100, String::from("Hello, world!"), tags, @@ -277,8 +351,73 @@ mod tests { None, ); ctx.predecessor_account_id = alice(); + ctx.block_timestamp = MILI_SECOND * 3; + testing_env!(ctx.clone()); + ctr.respond(poll_id, vec![Some(Answer::YesNo(true)), None, None]); + ctx.predecessor_account_id = bob(); testing_env!(ctx.clone()); - let answers = vec![Some(Answer::YesNo(true))]; - ctr.respond(poll_id, answers); + ctr.respond(poll_id, vec![Some(Answer::YesNo(true)), None, None]); + ctx.predecessor_account_id = charlie(); + testing_env!(ctx.clone()); + ctr.respond(poll_id, vec![Some(Answer::YesNo(false)), None, None]); + let results = ctr.results(poll_id); + assert_eq!( + results, + Results { + status: Status::Active, + number_of_participants: 3, + results: vec![ + PollResult::YesNo((2, 1)), + PollResult::TextChoices(vec![]), + PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }) + ] + } + ) + } + + #[test] + fn opinion_scale_flow() { + let tags = vec![String::from("tag1"), String::from("tag2")]; + let (mut ctx, mut ctr) = setup(&admin()); + let poll_id = ctr.create_poll( + false, + questions(), + 2, + 100, + String::from("Multiple questions test!"), + tags, + None, + None, + ); + ctx.predecessor_account_id = alice(); + ctx.block_timestamp = (MILI_SECOND * 3); + testing_env!(ctx.clone()); + ctr.respond( + poll_id, + vec![ + Some(Answer::YesNo(true)), + None, + Some(Answer::OpinionScale(5)), + ], + ); + ctx.predecessor_account_id = bob(); + testing_env!(ctx.clone()); + ctr.respond(poll_id, vec![None, None, Some(Answer::OpinionScale(10))]); + ctx.predecessor_account_id = charlie(); + testing_env!(ctx.clone()); + ctr.respond(poll_id, vec![None, None, Some(Answer::OpinionScale(2))]); + let results = ctr.results(poll_id); + assert_eq!( + results, + Results { + status: Status::Active, + number_of_participants: 3, + results: vec![ + PollResult::YesNo((1, 0)), + PollResult::TextChoices(vec![]), + PollResult::OpinionScale(OpinionScaleResult { sum: 17, num: 3 }) + ] + } + ) } } diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 8be96770..0b58096a 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -15,6 +15,7 @@ pub enum Answer { TextAnswer(String), } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] pub enum PollResult { YesNo((u32, u32)), // yes, no @@ -24,6 +25,7 @@ pub enum PollResult { TextAnswer(Vec), } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] pub struct OpinionScaleResult { pub sum: u32, @@ -34,13 +36,13 @@ pub struct OpinionScaleResult { #[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize)] #[serde(crate = "near_sdk::serde")] pub struct Question { - pub question_type: Answer, // required + pub question_type: Answer, // required pub required: bool, // required, if true users can't vote without having an answer for this question pub title: String, // required pub description: Option, // optional pub image: Option, // optional pub labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question - // pub choices: Option>, // if applicable, choices for the text and picture choices question TODO: make sure we dont need it + pub choices: Option>, // if applicable, choices for the text and picture choices question TODO: make sure we dont need it } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] @@ -65,6 +67,7 @@ pub struct PollResponse { } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] +#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] pub struct Results { pub status: Status, @@ -81,6 +84,7 @@ pub struct Answers { } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] +#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] pub enum Status { NotStarted, From bffd7811647e03eabb85626adcbc121b1f3678ed Mon Sep 17 00:00:00 2001 From: sczembor Date: Tue, 8 Aug 2023 20:18:50 +0200 Subject: [PATCH 07/42] improve the respond implementation --- contracts/easy-poll/src/lib.rs | 209 ++++++++++++++++++++++++----- contracts/easy-poll/src/storage.rs | 9 +- 2 files changed, 180 insertions(+), 38 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 5b3049d6..d9cc0741 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -1,6 +1,3 @@ -use std::fmt::format; -use std::future::Future; - use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LookupMap, UnorderedMap}; use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; @@ -54,7 +51,8 @@ impl Contract { .expect("respond not found") } - // returns None if poll is not found + /// returns None if poll is not found + /// this should result all the restuls but `TextAnswers` pub fn results(&self, poll_id: u64) -> Results { self.results.get(&poll_id).expect("poll not found") } @@ -140,41 +138,45 @@ impl Contract { } match (&answers[i], &poll_results.results[i]) { - (Some(Answer::YesNo(yes_no)), PollResult::YesNo((yes, no))) => { - if *yes_no { + (Some(Answer::YesNo(answer)), PollResult::YesNo((yes, no))) => { + if *answer { poll_results.results[i] = PollResult::YesNo((*yes + 1, *no)); } else { poll_results.results[i] = PollResult::YesNo((*yes, *no + 1)); } } - (Some(Answer::TextChoices(value)), PollResult::TextChoices(vector)) => { - let mut new_vec = Vec::new(); - for i in value { - new_vec[*i] = vector[*i] + *i as u32; + (Some(Answer::TextChoices(answer)), PollResult::TextChoices(results)) => { + let mut res: Vec = results.to_vec(); + for i in 0..answer.len() { + if answer[i] == true { + res[i] += 1; + } } - poll_results.results[i] = PollResult::TextChoices(new_vec); + poll_results.results[i] = PollResult::TextChoices(res); } - (Some(Answer::PictureChoices(value)), PollResult::PictureChoices(vector)) => { - let mut new_vec = Vec::new(); - for i in value { - new_vec[*i] = vector[*i] + *i as u32; + (Some(Answer::PictureChoices(answer)), PollResult::PictureChoices(results)) => { + let mut res: Vec = results.to_vec(); + for i in 0..answer.len() { + if answer[i] == true { + res[i] += 1; + } } - poll_results.results[i] = PollResult::PictureChoices(new_vec); + poll_results.results[i] = PollResult::PictureChoices(res); } - (Some(Answer::OpinionScale(value)), PollResult::OpinionScale(opinion)) => { - if *value > 10 { + (Some(Answer::OpinionScale(answer)), PollResult::OpinionScale(results)) => { + if *answer > 10 { env::panic_str("opinion must be between 0 and 10"); } poll_results.results[i] = PollResult::OpinionScale(OpinionScaleResult { - sum: opinion.sum + *value as u32, - num: opinion.num + 1 as u32, + sum: results.sum + *answer as u32, + num: results.num + 1 as u32, }); } - // (Some(Answer::TextAnswer(answer)), Result::TextAnswer(answers)) => { - // let mut new_vec = Vec::new(); - // new_vec = *answers; - // new_vec.push(*answer); - // } + (Some(Answer::TextAnswer(answer)), PollResult::TextAnswer(results)) => { + let mut results = results.clone(); + results.push(answer.clone()); + poll_results.results[i] = PollResult::TextAnswer(results); + } (_, _) => (), } if answers[i].is_some() { @@ -233,7 +235,9 @@ impl Contract { for question in questions { results.push(match question.question_type { Answer::YesNo(_) => PollResult::YesNo((0, 0)), - Answer::TextChoices(_) => PollResult::TextChoices(Vec::new()), + Answer::TextChoices(_) => { + PollResult::TextChoices(vec![0; question.choices.clone().unwrap().len()]) + } Answer::PictureChoices(_) => PollResult::PictureChoices(Vec::new()), Answer::OpinionScale(_) => { PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }) @@ -291,9 +295,10 @@ mod tests { image: None, labels: None, choices: None, + max_choices: None, }); questions.push(Question { - question_type: Answer::TextChoices(vec![0, 0, 0]), + question_type: Answer::TextChoices(vec![false, false, false]), required: false, title: String::from("Yes and no test!"), description: None, @@ -304,6 +309,7 @@ mod tests { String::from("disagree"), String::from("no opinion"), ]), + max_choices: Some(1), }); questions.push(Question { question_type: Answer::OpinionScale(0), @@ -313,6 +319,17 @@ mod tests { image: None, labels: None, choices: None, + max_choices: None, + }); + questions.push(Question { + question_type: Answer::TextAnswer(String::from("")), + required: false, + title: String::from("Opinion test!"), + description: None, + image: None, + labels: None, + choices: None, + max_choices: None, }); questions } @@ -353,13 +370,13 @@ mod tests { ctx.predecessor_account_id = alice(); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx.clone()); - ctr.respond(poll_id, vec![Some(Answer::YesNo(true)), None, None]); + ctr.respond(poll_id, vec![Some(Answer::YesNo(true)), None, None, None]); ctx.predecessor_account_id = bob(); testing_env!(ctx.clone()); - ctr.respond(poll_id, vec![Some(Answer::YesNo(true)), None, None]); + ctr.respond(poll_id, vec![Some(Answer::YesNo(true)), None, None, None]); ctx.predecessor_account_id = charlie(); testing_env!(ctx.clone()); - ctr.respond(poll_id, vec![Some(Answer::YesNo(false)), None, None]); + ctr.respond(poll_id, vec![Some(Answer::YesNo(false)), None, None, None]); let results = ctr.results(poll_id); assert_eq!( results, @@ -369,7 +386,8 @@ mod tests { results: vec![ PollResult::YesNo((2, 1)), PollResult::TextChoices(vec![]), - PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }) + PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), + PollResult::TextAnswer(vec![]) ] } ) @@ -398,14 +416,21 @@ mod tests { Some(Answer::YesNo(true)), None, Some(Answer::OpinionScale(5)), + None, ], ); ctx.predecessor_account_id = bob(); testing_env!(ctx.clone()); - ctr.respond(poll_id, vec![None, None, Some(Answer::OpinionScale(10))]); + ctr.respond( + poll_id, + vec![None, None, Some(Answer::OpinionScale(10)), None], + ); ctx.predecessor_account_id = charlie(); testing_env!(ctx.clone()); - ctr.respond(poll_id, vec![None, None, Some(Answer::OpinionScale(2))]); + ctr.respond( + poll_id, + vec![None, None, Some(Answer::OpinionScale(2)), None], + ); let results = ctr.results(poll_id); assert_eq!( results, @@ -415,7 +440,123 @@ mod tests { results: vec![ PollResult::YesNo((1, 0)), PollResult::TextChoices(vec![]), - PollResult::OpinionScale(OpinionScaleResult { sum: 17, num: 3 }) + PollResult::OpinionScale(OpinionScaleResult { sum: 17, num: 3 }), + PollResult::TextAnswer(vec![]) + ] + } + ) + } + #[test] + fn text_chocies_flow() { + let tags = vec![String::from("tag1"), String::from("tag2")]; + let (mut ctx, mut ctr) = setup(&admin()); + let poll_id = ctr.create_poll( + false, + questions(), + 2, + 100, + String::from("Hello, world!"), + tags, + None, + None, + ); + ctx.predecessor_account_id = alice(); + ctx.block_timestamp = MILI_SECOND * 3; + testing_env!(ctx.clone()); + ctr.respond( + poll_id, + vec![ + None, + Some(Answer::TextChoices(vec![true, false, false])), + None, + None, + ], + ); + ctx.predecessor_account_id = bob(); + testing_env!(ctx.clone()); + ctr.respond( + poll_id, + vec![ + None, + Some(Answer::TextChoices(vec![true, false, false])), + None, + None, + ], + ); + ctx.predecessor_account_id = charlie(); + testing_env!(ctx.clone()); + ctr.respond( + poll_id, + vec![ + None, + Some(Answer::TextChoices(vec![false, true, false])), + None, + None, + ], + ); + let results = ctr.results(poll_id); + assert_eq!( + results, + Results { + status: Status::Active, + number_of_participants: 3, + results: vec![ + PollResult::YesNo((0, 0)), + PollResult::TextChoices(vec![2, 1, 0]), + PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), + PollResult::TextAnswer(vec![]) + ] + } + ) + } + + #[test] + fn text_answers_flow() { + let tags = vec![String::from("tag1"), String::from("tag2")]; + let (mut ctx, mut ctr) = setup(&admin()); + let poll_id = ctr.create_poll( + false, + questions(), + 2, + 100, + String::from("Hello, world!"), + tags, + None, + None, + ); + ctx.predecessor_account_id = alice(); + ctx.block_timestamp = MILI_SECOND * 3; + testing_env!(ctx.clone()); + let answer1: String = "Answer 1".to_string(); + let answer2: String = "Answer 2".to_string(); + let answer3: String = "Answer 3".to_string(); + ctr.respond( + poll_id, + vec![None, None, None, Some(Answer::TextAnswer(answer1.clone()))], + ); + ctx.predecessor_account_id = bob(); + testing_env!(ctx.clone()); + ctr.respond( + poll_id, + vec![None, None, None, Some(Answer::TextAnswer(answer2.clone()))], + ); + ctx.predecessor_account_id = charlie(); + testing_env!(ctx.clone()); + ctr.respond( + poll_id, + vec![None, None, None, Some(Answer::TextAnswer(answer3.clone()))], + ); + let results = ctr.results(poll_id); + assert_eq!( + results, + Results { + status: Status::Active, + number_of_participants: 3, + results: vec![ + PollResult::YesNo((0, 0)), + PollResult::TextChoices(vec![]), + PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), + PollResult::TextAnswer(vec![answer1, answer2, answer3]) ] } ) diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 0b58096a..e07f2b46 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -1,6 +1,6 @@ use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::serde::{Deserialize, Serialize}; -use near_sdk::{AccountId, BorshStorageKey}; +use near_sdk::BorshStorageKey; pub type PollId = u64; @@ -9,9 +9,9 @@ pub type PollId = u64; #[serde(crate = "near_sdk::serde")] pub enum Answer { YesNo(bool), - TextChoices(Vec), // should respect the min_choices, max_choices - PictureChoices(Vec), // should respect the min_choices, max_choices - OpinionScale(u64), // should be a number between 0 and 10 + TextChoices(Vec), // should respect the min_choices, max_choices + PictureChoices(Vec), // should respect the min_choices, max_choices + OpinionScale(u64), // should be a number between 0 and 10 TextAnswer(String), } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] @@ -43,6 +43,7 @@ pub struct Question { pub image: Option, // optional pub labels: Option<(String, String, String)>, // if applicable, labels for the opinion scale question pub choices: Option>, // if applicable, choices for the text and picture choices question TODO: make sure we dont need it + pub max_choices: Option, } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] From 7929cf14e9b304b2fc180c52c1c3292f0b4d0f28 Mon Sep 17 00:00:00 2001 From: sczembor Date: Wed, 9 Aug 2023 18:55:41 +0200 Subject: [PATCH 08/42] add corss contract call to registry --- contracts/easy-poll/src/constants.rs | 23 ++++ contracts/easy-poll/src/errors.rs | 4 + contracts/easy-poll/src/ext.rs | 20 +++ contracts/easy-poll/src/lib.rs | 175 ++++++++++++++++++++------- 4 files changed, 181 insertions(+), 41 deletions(-) create mode 100644 contracts/easy-poll/src/constants.rs create mode 100644 contracts/easy-poll/src/ext.rs diff --git a/contracts/easy-poll/src/constants.rs b/contracts/easy-poll/src/constants.rs new file mode 100644 index 00000000..8c023fab --- /dev/null +++ b/contracts/easy-poll/src/constants.rs @@ -0,0 +1,23 @@ +use near_sdk::{Balance, Gas}; + +pub const MICRO_NEAR: Balance = 1_000_000_000_000_000_000; // 1e19 yoctoNEAR +pub const MILI_NEAR: Balance = 1_000 * MICRO_NEAR; + +/// 1s in nano seconds. +pub const SECOND: u64 = 1_000_000_000; +/// 1ms in nano seconds. +pub const MSECOND: u64 = 1_000_000; + +pub const GAS_NOMINATE: Gas = Gas(20 * Gas::ONE_TERA.0); +pub const GAS_UPVOTE: Gas = Gas(20 * Gas::ONE_TERA.0); +pub const GAS_COMMENT: Gas = Gas(20 * Gas::ONE_TERA.0); + +/// nomination: (accountID, HouseType) -> (25 bytes + 24 bytes) = 49 bytes < 100 bytes +pub const NOMINATE_COST: Balance = MILI_NEAR; + +/// upvote: (accountID, Account) -> (25 bytes + 25 bytes) = 50 bytes +/// upvotes_per_candidate: (accountID, u32) -> (25 bytes + 4 bytes) = 29 bytes +/// sum = 50 + 29 = 79 bytes < 100 bytes +pub const UPVOTE_COST: Balance = MILI_NEAR; + +pub const MAX_CAMPAIGN_LEN: usize = 200; diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs index d3cdc752..fb8dc19b 100644 --- a/contracts/easy-poll/src/errors.rs +++ b/contracts/easy-poll/src/errors.rs @@ -1,6 +1,8 @@ use near_sdk::env::panic_str; use near_sdk::FunctionError; +use crate::Poll; + /// Contract errors #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq))] #[derive(Debug)] @@ -9,6 +11,7 @@ pub enum PollError { NoSBTs, NotFound, NotActive, + OpinionScale, } impl FunctionError for PollError { @@ -20,6 +23,7 @@ impl FunctionError for PollError { PollError::NoSBTs => panic_str("voter is not a verified human"), PollError::NotFound => panic_str("poll not found"), PollError::NotActive => panic_str("poll is not active"), + PollError::OpinionScale => panic_str("opinion must be between 0 and 10"), } } } diff --git a/contracts/easy-poll/src/ext.rs b/contracts/easy-poll/src/ext.rs new file mode 100644 index 00000000..4fe85f40 --- /dev/null +++ b/contracts/easy-poll/src/ext.rs @@ -0,0 +1,20 @@ +pub use crate::storage::*; +use near_sdk::{ext_contract, AccountId}; +use sbt::TokenId; + +use crate::PollError; + +#[ext_contract(ext_registry)] +trait ExtRegistry { + // queries + + fn is_human(&self, account: AccountId) -> Vec<(AccountId, Vec)>; +} +// #[ext_contract(ext_self)] +// pub trait ExtSelf { +// fn on_human_verifed( +// &mut self, +// poll_id: PollId, +// answers: Vec>, +// ) -> Result<(), PollError>; +// } diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index d9cc0741..ae598997 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -1,11 +1,16 @@ +use ext::ext_registry; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; use near_sdk::collections::{LookupMap, UnorderedMap}; use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; +pub use crate::constants::*; pub use crate::errors::PollError; +pub use crate::ext::*; pub use crate::storage::*; +mod constants; mod errors; +mod ext; mod storage; #[near_bindgen] @@ -123,18 +128,57 @@ impl Contract { answers: Vec>, ) -> Result<(), PollError> { let caller = env::predecessor_account_id(); - // check if poll exists and is active - self.assert_active(poll_id); + + match self.assert_active(poll_id) { + Err(err) => return Err(err), + Ok(_) => (), + }; + + // TODO: I think we should add a option for the poll creator to choose whether changing + // the answers while the poll is active is allowed or not self.assert_answered(poll_id, &caller); + let poll = self.polls.get(&poll_id).unwrap(); // if iah calls the registry to verify the iah sbt - self.assert_human(poll_id, &caller); + if poll.iah_only { + ext_registry::ext(self.registry.clone()) + .is_human(caller.clone()) + .then( + Self::ext(env::current_account_id()) + .with_static_gas(GAS_UPVOTE) + .on_human_verifed(true, caller, poll_id, answers), + ); + } else { + Self::ext(env::current_account_id()) + .with_static_gas(GAS_UPVOTE) + .on_human_verifed(false, caller, poll_id, answers); + } + Ok(()) + } + + /********** + * PRIVATE + **********/ + + #[private] + #[handle_result] + pub fn on_human_verifed( + &mut self, + #[callback_unwrap] tokens: Vec<(AccountId, Vec)>, + iah_only: bool, + caller: AccountId, + poll_id: PollId, + answers: Vec>, + ) -> Result<(), PollError> { + if iah_only && tokens.is_empty() { + return Err(PollError::NoSBTs); + } let questions: Vec = self.polls.get(&poll_id).unwrap().questions; let mut unwrapped_answers: Vec = Vec::new(); let mut poll_results = self.results.get(&poll_id).unwrap(); // let mut results = poll_results.results; for i in 0..questions.len() { if questions[i].required && answers[i].is_none() { - env::panic_str(format!("poll question {} requires an answer", i).as_str()); + return Err(PollError::RequiredAnswer); } match (&answers[i], &poll_results.results[i]) { @@ -165,7 +209,7 @@ impl Contract { } (Some(Answer::OpinionScale(answer)), PollResult::OpinionScale(results)) => { if *answer > 10 { - env::panic_str("opinion must be between 0 and 10"); + return Err(PollError::OpinionScale); } poll_results.results[i] = PollResult::OpinionScale(OpinionScaleResult { sum: results.sum + *answer as u32, @@ -199,24 +243,17 @@ impl Contract { /********** * INTERNAL **********/ - fn assert_active(&self, poll_id: PollId) { - let poll = self.polls.get(&poll_id).expect("poll not found"); + #[handle_result] + fn assert_active(&self, poll_id: PollId) -> Result<(), PollError> { + let poll = match self.polls.get(&poll_id) { + Some(poll) => poll, + None => return Err(PollError::NotFound), + }; let current_timestamp = env::block_timestamp_ms(); - require!( - poll.starts_at < current_timestamp, - format!( - "poll have not started yet, start_at: {:?}, current_timestamp: {:?}", - poll.starts_at, current_timestamp - ) - ); - require!(poll.ends_at > current_timestamp, "poll not active"); - } - - fn assert_human(&self, poll_id: PollId, caller: &AccountId) { - let poll = self.polls.get(&poll_id).unwrap(); - if poll.iah_only { - // TODO: call registry to verify humanity + if poll.starts_at < current_timestamp || poll.ends_at > current_timestamp { + return Err(PollError::NotActive); } + Ok(()) } fn assert_admin(&self) { @@ -258,8 +295,7 @@ impl Contract { #[cfg(test)] mod tests { - use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, Balance, VMContext}; - use sbt::{ClassId, ContractMetadata, TokenMetadata}; + use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, VMContext}; use crate::{Answer, Contract, OpinionScaleResult, PollResult, Question, Results, Status}; @@ -341,7 +377,7 @@ mod tests { .is_view(false) .build(); testing_env!(ctx.clone()); - let mut ctr = Contract::new(admin(), registry()); + let ctr = Contract::new(admin(), registry()); ctx.predecessor_account_id = predecessor.clone(); testing_env!(ctx.clone()); return (ctx, ctr); @@ -349,7 +385,7 @@ mod tests { #[test] fn assert_admin() { - let (mut ctx, mut ctr) = setup(&admin()); + let (_, ctr) = setup(&admin()); ctr.assert_admin(); } @@ -370,13 +406,34 @@ mod tests { ctx.predecessor_account_id = alice(); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx.clone()); - ctr.respond(poll_id, vec![Some(Answer::YesNo(true)), None, None, None]); + let mut res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, + poll_id, + vec![Some(Answer::YesNo(true)), None, None, None], + ); + assert!(res.is_ok()); ctx.predecessor_account_id = bob(); testing_env!(ctx.clone()); - ctr.respond(poll_id, vec![Some(Answer::YesNo(true)), None, None, None]); + res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, + poll_id, + vec![Some(Answer::YesNo(true)), None, None, None], + ); + assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); testing_env!(ctx.clone()); - ctr.respond(poll_id, vec![Some(Answer::YesNo(false)), None, None, None]); + res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, + poll_id, + vec![Some(Answer::YesNo(false)), None, None, None], + ); + assert!(res.is_ok()); let results = ctr.results(poll_id); assert_eq!( results, @@ -385,7 +442,7 @@ mod tests { number_of_participants: 3, results: vec![ PollResult::YesNo((2, 1)), - PollResult::TextChoices(vec![]), + PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), PollResult::TextAnswer(vec![]) ] @@ -408,9 +465,12 @@ mod tests { None, ); ctx.predecessor_account_id = alice(); - ctx.block_timestamp = (MILI_SECOND * 3); + ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx.clone()); - ctr.respond( + let mut res = ctr.on_human_verifed( + vec![], + false, + alice(), poll_id, vec![ Some(Answer::YesNo(true)), @@ -419,18 +479,27 @@ mod tests { None, ], ); + assert!(res.is_ok()); ctx.predecessor_account_id = bob(); testing_env!(ctx.clone()); - ctr.respond( + res = ctr.on_human_verifed( + vec![], + false, + bob(), poll_id, vec![None, None, Some(Answer::OpinionScale(10)), None], ); + assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); testing_env!(ctx.clone()); - ctr.respond( + res = ctr.on_human_verifed( + vec![], + false, + charlie(), poll_id, vec![None, None, Some(Answer::OpinionScale(2)), None], ); + assert!(res.is_ok()); let results = ctr.results(poll_id); assert_eq!( results, @@ -439,7 +508,7 @@ mod tests { number_of_participants: 3, results: vec![ PollResult::YesNo((1, 0)), - PollResult::TextChoices(vec![]), + PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 17, num: 3 }), PollResult::TextAnswer(vec![]) ] @@ -463,7 +532,10 @@ mod tests { ctx.predecessor_account_id = alice(); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx.clone()); - ctr.respond( + let mut res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, poll_id, vec![ None, @@ -472,9 +544,13 @@ mod tests { None, ], ); + assert!(res.is_ok()); ctx.predecessor_account_id = bob(); testing_env!(ctx.clone()); - ctr.respond( + res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, poll_id, vec![ None, @@ -483,9 +559,13 @@ mod tests { None, ], ); + assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); testing_env!(ctx.clone()); - ctr.respond( + res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, poll_id, vec![ None, @@ -494,6 +574,7 @@ mod tests { None, ], ); + assert!(res.is_ok()); let results = ctr.results(poll_id); assert_eq!( results, @@ -530,22 +611,34 @@ mod tests { let answer1: String = "Answer 1".to_string(); let answer2: String = "Answer 2".to_string(); let answer3: String = "Answer 3".to_string(); - ctr.respond( + let mut res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, poll_id, vec![None, None, None, Some(Answer::TextAnswer(answer1.clone()))], ); + assert!(res.is_ok()); ctx.predecessor_account_id = bob(); testing_env!(ctx.clone()); - ctr.respond( + res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, poll_id, vec![None, None, None, Some(Answer::TextAnswer(answer2.clone()))], ); + assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); testing_env!(ctx.clone()); - ctr.respond( + res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, poll_id, vec![None, None, None, Some(Answer::TextAnswer(answer3.clone()))], ); + assert!(res.is_ok()); let results = ctr.results(poll_id); assert_eq!( results, @@ -554,7 +647,7 @@ mod tests { number_of_participants: 3, results: vec![ PollResult::YesNo((0, 0)), - PollResult::TextChoices(vec![]), + PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), PollResult::TextAnswer(vec![answer1, answer2, answer3]) ] From 73b55b113a465a6745a47d6fcd2fb65553436af3 Mon Sep 17 00:00:00 2001 From: sczembor Date: Thu, 10 Aug 2023 19:49:18 +0200 Subject: [PATCH 09/42] implement text_answers result method --- contracts/easy-poll/src/lib.rs | 99 ++++++++++++++++++++++-------- contracts/easy-poll/src/storage.rs | 11 ++-- 2 files changed, 80 insertions(+), 30 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index ae598997..5d947828 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -22,6 +22,7 @@ pub struct Contract { pub polls: UnorderedMap, pub results: LookupMap, pub answers: UnorderedMap<(PollId, AccountId), Vec>, + pub text_answers: LookupMap<(PollId, usize), TextAnswers>, /// SBT registry. pub registry: AccountId, /// next poll id @@ -40,6 +41,7 @@ impl Contract { polls: UnorderedMap::new(StorageKey::Polls), results: LookupMap::new(StorageKey::Results), answers: UnorderedMap::new(StorageKey::Answers), + text_answers: LookupMap::new(StorageKey::TextAnswers), registry, next_poll_id: 0, } @@ -62,18 +64,46 @@ impl Contract { self.results.get(&poll_id).expect("poll not found") } + pub fn result_text_answers( + &self, + poll_id: u64, + question: usize, + from_answer: usize, + ) -> (bool, Vec) { + self._result_answers(poll_id, question, from_answer, 10) + } + // TODO: limit the max lenght of single answer and based on that return a fixed value of answers // Function must be called until true is returned -> meaning all the answers were returned // returns None if poll is not found // `question` must be an index of the text question in the poll - pub fn result_answers( + pub fn _result_answers( &self, - poll_id: usize, + poll_id: u64, question: usize, - from_answer: u64, - ) -> (Vec, bool) { - //TODO check if question is type `TextAnswer` - unimplemented!(); + from_answer: usize, + limit: usize, + ) -> (bool, Vec) { + self.polls + .get(&poll_id) + .expect("poll not found") + .questions + .get(question) + .expect("question not type `TextAnswer`"); + let text_answers = self + .text_answers + .get(&(poll_id, question)) + .expect("no answer found") + .answers; + let to_return; + let mut finished = false; + if from_answer + limit > text_answers.len() { + to_return = text_answers[from_answer..].to_vec(); + finished = true; + } else { + to_return = text_answers[from_answer..from_answer + limit].to_vec(); + } + (finished, to_return) } // user can update the poll if starts_at > now // it panics if @@ -216,10 +246,10 @@ impl Contract { num: results.num + 1 as u32, }); } - (Some(Answer::TextAnswer(answer)), PollResult::TextAnswer(results)) => { - let mut results = results.clone(); - results.push(answer.clone()); - poll_results.results[i] = PollResult::TextAnswer(results); + (Some(Answer::TextAnswer(answer)), _) => { + let mut text_answers = self.text_answers.get(&(poll_id, i)).expect("not found"); + text_answers.answers.push(answer.clone()); + self.text_answers.insert(&(poll_id, i), &text_answers); } (_, _) => (), } @@ -269,18 +299,36 @@ impl Contract { fn initalize_results(&mut self, poll_id: PollId, questions: &Vec) { let mut results = Vec::new(); + let mut index = 0; for question in questions { - results.push(match question.question_type { - Answer::YesNo(_) => PollResult::YesNo((0, 0)), - Answer::TextChoices(_) => { - PollResult::TextChoices(vec![0; question.choices.clone().unwrap().len()]) - } - Answer::PictureChoices(_) => PollResult::PictureChoices(Vec::new()), + match question.question_type { + Answer::YesNo(_) => results.push(PollResult::YesNo((0, 0))), + Answer::TextChoices(_) => results.push(PollResult::TextChoices(vec![ + 0; + question + .choices + .clone() + .unwrap() + .len() + ])), + Answer::PictureChoices(_) => results.push(PollResult::PictureChoices(Vec::new())), Answer::OpinionScale(_) => { - PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }) + results.push(PollResult::OpinionScale(OpinionScaleResult { + sum: 0, + num: 0, + })) + } + Answer::TextAnswer(_) => { + results.push(PollResult::TextAnswer(true)); + self.text_answers.insert( + &(poll_id, index), + &TextAnswers { + answers: Vec::new(), + }, + ); } - Answer::TextAnswer(_) => PollResult::TextAnswer(Vec::new()), - }); + }; + index += 1; } self.results.insert( &poll_id, @@ -444,7 +492,7 @@ mod tests { PollResult::YesNo((2, 1)), PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer(vec![]) + PollResult::TextAnswer(true) ] } ) @@ -510,7 +558,7 @@ mod tests { PollResult::YesNo((1, 0)), PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 17, num: 3 }), - PollResult::TextAnswer(vec![]) + PollResult::TextAnswer(true) ] } ) @@ -585,7 +633,7 @@ mod tests { PollResult::YesNo((0, 0)), PollResult::TextChoices(vec![2, 1, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer(vec![]) + PollResult::TextAnswer(true) ] } ) @@ -649,9 +697,12 @@ mod tests { PollResult::YesNo((0, 0)), PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer(vec![answer1, answer2, answer3]) + PollResult::TextAnswer(true) ] } - ) + ); + let text_answers = ctr.result_text_answers(poll_id, 3, 0); + assert!(text_answers.0); + assert_eq!(text_answers.1, vec![answer1, answer2, answer3]) } } diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index e07f2b46..99ccff0a 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -22,7 +22,7 @@ pub enum PollResult { TextChoices(Vec), // should respect the min_choices, max_choices PictureChoices(Vec), // should respect the min_choices, max_choices OpinionScale(OpinionScaleResult), // mean value - TextAnswer(Vec), + TextAnswer(bool), // indicates whether the question exist or not, the answers are stored in a different struct called `TextAnswers` } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] @@ -76,12 +76,10 @@ pub struct Results { pub results: Vec, // question_id, result (sum of yes etc.) } -#[derive(Serialize, Clone)] +#[derive(BorshSerialize, BorshDeserialize, Serialize, Clone)] #[serde(crate = "near_sdk::serde")] -pub struct Answers { - status: Status, - number_of_participants: u64, - answers: Vec<(usize, Vec)>, // question_id, list of answers +pub struct TextAnswers { + pub answers: Vec, // question_id, list of answers } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] @@ -98,4 +96,5 @@ pub enum StorageKey { Polls, Results, Answers, + TextAnswers, } From ecd2a830eb49dba44323a302c3cf45d3c98d9fef Mon Sep 17 00:00:00 2001 From: sczembor Date: Fri, 11 Aug 2023 15:21:12 +0200 Subject: [PATCH 10/42] update the structure --- contracts/easy-poll/src/constants.rs | 23 ---------- contracts/easy-poll/src/lib.rs | 66 ++++++++++++++-------------- contracts/easy-poll/src/storage.rs | 8 ++-- 3 files changed, 38 insertions(+), 59 deletions(-) delete mode 100644 contracts/easy-poll/src/constants.rs diff --git a/contracts/easy-poll/src/constants.rs b/contracts/easy-poll/src/constants.rs deleted file mode 100644 index 8c023fab..00000000 --- a/contracts/easy-poll/src/constants.rs +++ /dev/null @@ -1,23 +0,0 @@ -use near_sdk::{Balance, Gas}; - -pub const MICRO_NEAR: Balance = 1_000_000_000_000_000_000; // 1e19 yoctoNEAR -pub const MILI_NEAR: Balance = 1_000 * MICRO_NEAR; - -/// 1s in nano seconds. -pub const SECOND: u64 = 1_000_000_000; -/// 1ms in nano seconds. -pub const MSECOND: u64 = 1_000_000; - -pub const GAS_NOMINATE: Gas = Gas(20 * Gas::ONE_TERA.0); -pub const GAS_UPVOTE: Gas = Gas(20 * Gas::ONE_TERA.0); -pub const GAS_COMMENT: Gas = Gas(20 * Gas::ONE_TERA.0); - -/// nomination: (accountID, HouseType) -> (25 bytes + 24 bytes) = 49 bytes < 100 bytes -pub const NOMINATE_COST: Balance = MILI_NEAR; - -/// upvote: (accountID, Account) -> (25 bytes + 25 bytes) = 50 bytes -/// upvotes_per_candidate: (accountID, u32) -> (25 bytes + 4 bytes) = 29 bytes -/// sum = 50 + 29 = 79 bytes < 100 bytes -pub const UPVOTE_COST: Balance = MILI_NEAR; - -pub const MAX_CAMPAIGN_LEN: usize = 200; diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 5d947828..dcaeb8e0 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -1,18 +1,20 @@ -use ext::ext_registry; -use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::collections::{LookupMap, UnorderedMap}; -use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; - -pub use crate::constants::*; pub use crate::errors::PollError; pub use crate::ext::*; pub use crate::storage::*; +use cost::MILI_NEAR; +use ext::ext_registry; +use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::{LookupMap, UnorderedMap, Vector}; +use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; +use near_sdk::{Balance, Gas}; -mod constants; mod errors; mod ext; mod storage; +pub const RESPOND_COST: Balance = MILI_NEAR; +pub const RESPOND_CALLBACK_GAS: Gas = Gas(2 * Gas::ONE_TERA.0); + #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] pub struct Contract { @@ -22,7 +24,7 @@ pub struct Contract { pub polls: UnorderedMap, pub results: LookupMap, pub answers: UnorderedMap<(PollId, AccountId), Vec>, - pub text_answers: LookupMap<(PollId, usize), TextAnswers>, + pub text_answers: LookupMap<(PollId, usize), Vector>, /// SBT registry. pub registry: AccountId, /// next poll id @@ -70,7 +72,7 @@ impl Contract { question: usize, from_answer: usize, ) -> (bool, Vec) { - self._result_answers(poll_id, question, from_answer, 10) + self._result_answers(poll_id, question, from_answer, 20) } // TODO: limit the max lenght of single answer and based on that return a fixed value of answers @@ -93,15 +95,14 @@ impl Contract { let text_answers = self .text_answers .get(&(poll_id, question)) - .expect("no answer found") - .answers; + .expect("no answer found"); let to_return; let mut finished = false; - if from_answer + limit > text_answers.len() { - to_return = text_answers[from_answer..].to_vec(); + if from_answer + limit > text_answers.len() as usize { + to_return = text_answers.to_vec()[from_answer..].to_vec(); finished = true; } else { - to_return = text_answers[from_answer..from_answer + limit].to_vec(); + to_return = text_answers.to_vec()[from_answer..from_answer + limit].to_vec(); } (finished, to_return) } @@ -151,12 +152,17 @@ impl Contract { // - poll not active // - poll.verified_humans_only is true, and user is not verified on IAH // - user tries to vote with an invalid answer to a question + #[payable] #[handle_result] pub fn respond( &mut self, poll_id: PollId, answers: Vec>, ) -> Result<(), PollError> { + require!( + env::attached_deposit() >= RESPOND_COST, + "attached_deposit not sufficient" + ); let caller = env::predecessor_account_id(); match self.assert_active(poll_id) { @@ -174,12 +180,12 @@ impl Contract { .is_human(caller.clone()) .then( Self::ext(env::current_account_id()) - .with_static_gas(GAS_UPVOTE) + .with_static_gas(RESPOND_CALLBACK_GAS) .on_human_verifed(true, caller, poll_id, answers), ); } else { Self::ext(env::current_account_id()) - .with_static_gas(GAS_UPVOTE) + .with_static_gas(RESPOND_CALLBACK_GAS) .on_human_verifed(false, caller, poll_id, answers); } Ok(()) @@ -242,14 +248,14 @@ impl Contract { return Err(PollError::OpinionScale); } poll_results.results[i] = PollResult::OpinionScale(OpinionScaleResult { - sum: results.sum + *answer as u32, - num: results.num + 1 as u32, + sum: results.sum + *answer as u64, + num: results.num + 1 as u64, }); } (Some(Answer::TextAnswer(answer)), _) => { - let mut text_answers = self.text_answers.get(&(poll_id, i)).expect("not found"); - text_answers.answers.push(answer.clone()); - self.text_answers.insert(&(poll_id, i), &text_answers); + let mut answers = self.text_answers.get(&(poll_id, i)).expect("not found"); + answers.push(answer); + self.text_answers.insert(&(poll_id, i), &answers); } (_, _) => (), } @@ -319,13 +325,9 @@ impl Contract { })) } Answer::TextAnswer(_) => { - results.push(PollResult::TextAnswer(true)); - self.text_answers.insert( - &(poll_id, index), - &TextAnswers { - answers: Vec::new(), - }, - ); + results.push(PollResult::TextAnswer); + self.text_answers + .insert(&(poll_id, index), &Vector::new(StorageKey::TextAnswers)); } }; index += 1; @@ -492,7 +494,7 @@ mod tests { PollResult::YesNo((2, 1)), PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer(true) + PollResult::TextAnswer ] } ) @@ -558,7 +560,7 @@ mod tests { PollResult::YesNo((1, 0)), PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 17, num: 3 }), - PollResult::TextAnswer(true) + PollResult::TextAnswer ] } ) @@ -633,7 +635,7 @@ mod tests { PollResult::YesNo((0, 0)), PollResult::TextChoices(vec![2, 1, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer(true) + PollResult::TextAnswer ] } ) @@ -697,7 +699,7 @@ mod tests { PollResult::YesNo((0, 0)), PollResult::TextChoices(vec![0, 0, 0]), PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer(true) + PollResult::TextAnswer ] } ); diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 99ccff0a..61a4152f 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -11,7 +11,7 @@ pub enum Answer { YesNo(bool), TextChoices(Vec), // should respect the min_choices, max_choices PictureChoices(Vec), // should respect the min_choices, max_choices - OpinionScale(u64), // should be a number between 0 and 10 + OpinionScale(u8), // should be a number between 0 and 10 TextAnswer(String), } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] @@ -22,14 +22,14 @@ pub enum PollResult { TextChoices(Vec), // should respect the min_choices, max_choices PictureChoices(Vec), // should respect the min_choices, max_choices OpinionScale(OpinionScaleResult), // mean value - TextAnswer(bool), // indicates whether the question exist or not, the answers are stored in a different struct called `TextAnswers` + TextAnswer, // indicates whether the question exist or not, the answers are stored in a different struct called `TextAnswers` } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] pub struct OpinionScaleResult { - pub sum: u32, - pub num: u32, + pub sum: u64, + pub num: u64, } /// Helper structure for keys of the persistent collections. From e768996a6571d90a00b15ed67ad53dee43542da0 Mon Sep 17 00:00:00 2001 From: sczembor Date: Fri, 11 Aug 2023 15:21:49 +0200 Subject: [PATCH 11/42] merge --- contracts/Cargo.lock | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index d12cf183..35bd9e29 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -860,6 +860,24 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53aff6fdc1b181225acdcb5b14c47106726fd8e486707315b1b138baed68ee31" +[[package]] +name = "easy-poll" +version = "0.0.0" +dependencies = [ + "anyhow", + "cost", + "near-primitives 0.17.0", + "near-sdk", + "near-units", + "pretty_assertions", + "sbt", + "serde_json", + "tokio", + "tracing", + "uint", + "workspaces", +] + [[package]] name = "ed25519" version = "1.5.3" From da4476b910715fbdd4410360aaa44ea5b8a28a22 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:22:24 +0200 Subject: [PATCH 12/42] Update contracts/easy-poll/Cargo.toml Co-authored-by: Robert Zaremba --- contracts/easy-poll/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/easy-poll/Cargo.toml b/contracts/easy-poll/Cargo.toml index f83c3f7a..e9d216c2 100644 --- a/contracts/easy-poll/Cargo.toml +++ b/contracts/easy-poll/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "easy-poll" -version = "0.0.0" +version = "0.0.1" authors = [""] edition = { workspace = true } repository = { workspace = true } From 197e3bf747975f1a0ecaad2fd560101c06be7570 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Fri, 11 Aug 2023 15:23:11 +0200 Subject: [PATCH 13/42] Apply suggestions from code review Co-authored-by: Robert Zaremba --- contracts/easy-poll/Cargo.toml | 4 +++- contracts/easy-poll/README.md | 5 ++++- contracts/easy-poll/src/errors.rs | 2 +- contracts/easy-poll/src/storage.rs | 6 +++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/contracts/easy-poll/Cargo.toml b/contracts/easy-poll/Cargo.toml index e9d216c2..ee3f46ae 100644 --- a/contracts/easy-poll/Cargo.toml +++ b/contracts/easy-poll/Cargo.toml @@ -1,7 +1,9 @@ [package] name = "easy-poll" version = "0.0.1" -authors = [""] +authors = [ + "NDC GWG (https://near.social/#/mob.near/widget/ProfilePage?accountId=govworkinggroup.near)", +] edition = { workspace = true } repository = { workspace = true } license = { workspace = true } diff --git a/contracts/easy-poll/README.md b/contracts/easy-poll/README.md index f9c08c7b..57a2807c 100644 --- a/contracts/easy-poll/README.md +++ b/contracts/easy-poll/README.md @@ -1,4 +1,7 @@ -# Proof of concept for Easy Poll +# Easy Poll + +Proof of concept + Based on https://www.notion.so/near-ndc/EasyPoll-v2-f991a29781ca452db154c64922717d19#35d9a363be34495bb13ad5fa4b73cafe diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs index fb8dc19b..935f539e 100644 --- a/contracts/easy-poll/src/errors.rs +++ b/contracts/easy-poll/src/errors.rs @@ -11,7 +11,7 @@ pub enum PollError { NoSBTs, NotFound, NotActive, - OpinionScale, + OpinionRange, } impl FunctionError for PollError { diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 61a4152f..dcc60bd6 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -57,14 +57,14 @@ pub struct Poll { pub tags: Vec, // can be an empty vector pub description: Option, // optional pub link: Option, // optional - pub created_at: u64, // should be assigned by the smart contract not the user, time in milliseconds + pub created_at: u64, // time in milliseconds, should be assigned by the smart contract not a user. } #[derive(Deserialize, Serialize)] #[serde(crate = "near_sdk::serde")] pub struct PollResponse { answers: Vec<(usize, Answer)>, // question_id, answer - created_at: usize, // should be assigned by the smart contract not the user, time in milliseconds + created_at: u64, // time in milliseconds } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] @@ -72,7 +72,7 @@ pub struct PollResponse { #[serde(crate = "near_sdk::serde")] pub struct Results { pub status: Status, - pub number_of_participants: u64, + pub participants: u64, // number of participants pub results: Vec, // question_id, result (sum of yes etc.) } From bd867f83e5d6d814f603ccc1af22d86a519ffa1f Mon Sep 17 00:00:00 2001 From: sczembor Date: Fri, 11 Aug 2023 16:49:34 +0200 Subject: [PATCH 14/42] update the structs, apply code review suggestions; improve the unit tests --- contracts/Cargo.lock | 2 +- contracts/easy-poll/src/errors.rs | 4 +- contracts/easy-poll/src/ext.rs | 10 - contracts/easy-poll/src/lib.rs | 287 ++++++++++++++--------------- contracts/easy-poll/src/storage.rs | 16 +- 5 files changed, 148 insertions(+), 171 deletions(-) diff --git a/contracts/Cargo.lock b/contracts/Cargo.lock index 7880a828..2a65735b 100644 --- a/contracts/Cargo.lock +++ b/contracts/Cargo.lock @@ -862,7 +862,7 @@ checksum = "53aff6fdc1b181225acdcb5b14c47106726fd8e486707315b1b138baed68ee31" [[package]] name = "easy-poll" -version = "0.0.0" +version = "0.0.1" dependencies = [ "anyhow", "cost", diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs index 935f539e..4724ece7 100644 --- a/contracts/easy-poll/src/errors.rs +++ b/contracts/easy-poll/src/errors.rs @@ -1,8 +1,6 @@ use near_sdk::env::panic_str; use near_sdk::FunctionError; -use crate::Poll; - /// Contract errors #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq))] #[derive(Debug)] @@ -23,7 +21,7 @@ impl FunctionError for PollError { PollError::NoSBTs => panic_str("voter is not a verified human"), PollError::NotFound => panic_str("poll not found"), PollError::NotActive => panic_str("poll is not active"), - PollError::OpinionScale => panic_str("opinion must be between 0 and 10"), + PollError::OpinionRange => panic_str("opinion must be between 0 and 10"), } } } diff --git a/contracts/easy-poll/src/ext.rs b/contracts/easy-poll/src/ext.rs index 4fe85f40..8c90d3b7 100644 --- a/contracts/easy-poll/src/ext.rs +++ b/contracts/easy-poll/src/ext.rs @@ -2,19 +2,9 @@ pub use crate::storage::*; use near_sdk::{ext_contract, AccountId}; use sbt::TokenId; -use crate::PollError; - #[ext_contract(ext_registry)] trait ExtRegistry { // queries fn is_human(&self, account: AccountId) -> Vec<(AccountId, Vec)>; } -// #[ext_contract(ext_self)] -// pub trait ExtSelf { -// fn on_human_verifed( -// &mut self, -// poll_id: PollId, -// answers: Vec>, -// ) -> Result<(), PollError>; -// } diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index dcaeb8e0..fdce50ac 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -18,12 +18,13 @@ pub const RESPOND_CALLBACK_GAS: Gas = Gas(2 * Gas::ONE_TERA.0); #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] pub struct Contract { - /// Account authorized to add new minting authority - pub admin: AccountId, - /// map of classId -> to set of accounts authorized to mint + /// map of all polls pub polls: UnorderedMap, + /// map of all results summarized pub results: LookupMap, + /// map of all answers, (poll, user) -> vec of answers pub answers: UnorderedMap<(PollId, AccountId), Vec>, + /// text answers are stored in a separate map pub text_answers: LookupMap<(PollId, usize), Vector>, /// SBT registry. pub registry: AccountId, @@ -31,15 +32,11 @@ pub struct Contract { pub next_poll_id: PollId, } -// Implement the contract structure #[near_bindgen] impl Contract { - /// @admin: account authorized to add new minting authority - /// @ttl: time to live for SBT expire. Must be number in miliseconds. #[init] - pub fn new(admin: AccountId, registry: AccountId) -> Self { + pub fn new(registry: AccountId) -> Self { Self { - admin, polls: UnorderedMap::new(StorageKey::Polls), results: LookupMap::new(StorageKey::Results), answers: UnorderedMap::new(StorageKey::Answers), @@ -53,6 +50,7 @@ impl Contract { * QUERIES **********/ + /// Returns caller response to the specified poll pub fn my_respond(&self, poll_id: PollId) -> Vec { let caller = env::predecessor_account_id(); self.answers @@ -60,26 +58,25 @@ impl Contract { .expect("respond not found") } - /// returns None if poll is not found - /// this should result all the restuls but `TextAnswers` + /// Returns poll results (except for text answers), if poll not found panics pub fn results(&self, poll_id: u64) -> Results { self.results.get(&poll_id).expect("poll not found") } + /// Returns text answers in rounds. Starts from the question id provided. Needs to be called until true is returned. pub fn result_text_answers( &self, poll_id: u64, question: usize, from_answer: usize, ) -> (bool, Vec) { - self._result_answers(poll_id, question, from_answer, 20) + self._result_text_answers(poll_id, question, from_answer, 20) } - // TODO: limit the max lenght of single answer and based on that return a fixed value of answers + ///Returns a fixed value of answers // Function must be called until true is returned -> meaning all the answers were returned - // returns None if poll is not found // `question` must be an index of the text question in the poll - pub fn _result_answers( + pub fn _result_text_answers( &self, poll_id: u64, question: usize, @@ -106,10 +103,11 @@ impl Contract { } (finished, to_return) } - // user can update the poll if starts_at > now - // it panics if - // - user tries to create an invalid poll - // - if poll aready exists and starts_at < now + + /// User can update the poll if starts_at > now + /// it panics if + /// - user tries to create an invalid poll + /// - if poll aready exists and starts_at < now pub fn create_poll( &mut self, iah_only: bool, @@ -146,11 +144,11 @@ impl Contract { poll_id } - // user can change his answer when the poll is still active. - // it panics if - // - poll not found - // - poll not active - // - poll.verified_humans_only is true, and user is not verified on IAH + /// user can change his answer when the poll is still active. + /// it panics if + /// - poll not found + /// - poll not active + /// - poll.verified_humans_only is true, and user is not verified on IAH // - user tries to vote with an invalid answer to a question #[payable] #[handle_result] @@ -195,6 +193,7 @@ impl Contract { * PRIVATE **********/ + /// Callback for the respond method. #[private] #[handle_result] pub fn on_human_verifed( @@ -243,11 +242,11 @@ impl Contract { } poll_results.results[i] = PollResult::PictureChoices(res); } - (Some(Answer::OpinionScale(answer)), PollResult::OpinionScale(results)) => { + (Some(Answer::OpinionRange(answer)), PollResult::OpinionRange(results)) => { if *answer > 10 { - return Err(PollError::OpinionScale); + return Err(PollError::OpinionRange); } - poll_results.results[i] = PollResult::OpinionScale(OpinionScaleResult { + poll_results.results[i] = PollResult::OpinionRange(OpinionRangeResult { sum: results.sum + *answer as u64, num: results.num + 1 as u64, }); @@ -271,7 +270,7 @@ impl Contract { self.answers.insert(&(poll_id, caller), &answers); // update the status and number of participants poll_results.status = Status::Active; - poll_results.number_of_participants += 1; + poll_results.participants += 1; self.results.insert(&poll_id, &poll_results); Ok(()) } @@ -292,10 +291,6 @@ impl Contract { Ok(()) } - fn assert_admin(&self) { - require!(self.admin == env::predecessor_account_id(), "not an admin"); - } - fn assert_answered(&self, poll_id: PollId, caller: &AccountId) { require!( self.answers.get(&(poll_id, caller.clone())).is_none(), @@ -318,8 +313,8 @@ impl Contract { .len() ])), Answer::PictureChoices(_) => results.push(PollResult::PictureChoices(Vec::new())), - Answer::OpinionScale(_) => { - results.push(PollResult::OpinionScale(OpinionScaleResult { + Answer::OpinionRange(_) => { + results.push(PollResult::OpinionRange(OpinionRangeResult { sum: 0, num: 0, })) @@ -336,7 +331,7 @@ impl Contract { &poll_id, &Results { status: Status::NotStarted, - number_of_participants: 0, + participants: 0, results: results, }, ); @@ -347,7 +342,9 @@ impl Contract { mod tests { use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, VMContext}; - use crate::{Answer, Contract, OpinionScaleResult, PollResult, Question, Results, Status}; + use crate::{ + Answer, Contract, OpinionRangeResult, PollId, PollResult, Question, Results, Status, + }; const MILI_SECOND: u64 = 1000000; // nanoseconds @@ -367,25 +364,39 @@ mod tests { AccountId::new_unchecked("registry.near".to_string()) } - fn admin() -> AccountId { - AccountId::new_unchecked("admin.near".to_string()) + fn tags() -> Vec { + vec![String::from("tag1"), String::from("tag2")] + } + fn question_text_answers(required: bool) -> Question { + Question { + question_type: Answer::TextAnswer(String::from("")), + required, + title: String::from("Opinion test!"), + description: None, + image: None, + labels: None, + choices: None, + max_choices: None, + } } - fn questions() -> Vec { - let mut questions = Vec::new(); - questions.push(Question { + fn question_yes_no(required: bool) -> Question { + Question { question_type: Answer::YesNo(true), - required: false, + required, title: String::from("Yes and no test!"), description: None, image: None, labels: None, choices: None, max_choices: None, - }); - questions.push(Question { + } + } + + fn question_text_choices(required: bool) -> Question { + Question { question_type: Answer::TextChoices(vec![false, false, false]), - required: false, + required, title: String::from("Yes and no test!"), description: None, image: None, @@ -396,60 +407,66 @@ mod tests { String::from("no opinion"), ]), max_choices: Some(1), - }); - questions.push(Question { - question_type: Answer::OpinionScale(0), - required: false, - title: String::from("Opinion test!"), - description: None, - image: None, - labels: None, - choices: None, - max_choices: None, - }); - questions.push(Question { - question_type: Answer::TextAnswer(String::from("")), - required: false, + } + } + + fn question_opinion_range(required: bool) -> Question { + Question { + question_type: Answer::OpinionRange(0), + required, title: String::from("Opinion test!"), description: None, image: None, labels: None, choices: None, max_choices: None, - }); - questions + } + } + + fn mk_batch_text_answers( + ctr: &mut Contract, + predecessor: AccountId, + poll_id: PollId, + num_answers: u64, + ) { + for i in 0..num_answers { + let res = ctr.on_human_verifed( + vec![], + false, + predecessor.clone(), + poll_id, + vec![Some(Answer::TextAnswer(format!( + "Answer Answer Answer Answer Answer Answer Answer Answer Answer{}", + i + )))], + ); + assert!(res.is_ok()); + } } fn setup(predecessor: &AccountId) -> (VMContext, Contract) { let mut ctx = VMContextBuilder::new() - .predecessor_account_id(admin()) + .predecessor_account_id(alice()) .block_timestamp(MILI_SECOND) .is_view(false) .build(); testing_env!(ctx.clone()); - let ctr = Contract::new(admin(), registry()); + let ctr = Contract::new(registry()); ctx.predecessor_account_id = predecessor.clone(); testing_env!(ctx.clone()); return (ctx, ctr); } - #[test] - fn assert_admin() { - let (_, ctr) = setup(&admin()); - ctr.assert_admin(); - } - #[test] fn yes_no_flow() { - let tags = vec![String::from("tag1"), String::from("tag2")]; - let (mut ctx, mut ctr) = setup(&admin()); + let (mut ctx, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, - questions(), + vec![question_yes_no(true)], 2, 100, String::from("Hello, world!"), - tags, + tags(), None, None, ); @@ -461,7 +478,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![Some(Answer::YesNo(true)), None, None, None], + vec![Some(Answer::YesNo(true))], ); assert!(res.is_ok()); ctx.predecessor_account_id = bob(); @@ -471,7 +488,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![Some(Answer::YesNo(true)), None, None, None], + vec![Some(Answer::YesNo(true))], ); assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); @@ -481,7 +498,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![Some(Answer::YesNo(false)), None, None, None], + vec![Some(Answer::YesNo(false))], ); assert!(res.is_ok()); let results = ctr.results(poll_id); @@ -489,28 +506,22 @@ mod tests { results, Results { status: Status::Active, - number_of_participants: 3, - results: vec![ - PollResult::YesNo((2, 1)), - PollResult::TextChoices(vec![0, 0, 0]), - PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer - ] + participants: 3, + results: vec![PollResult::YesNo((2, 1)),] } ) } #[test] - fn opinion_scale_flow() { - let tags = vec![String::from("tag1"), String::from("tag2")]; - let (mut ctx, mut ctr) = setup(&admin()); + fn opinion_range_flow() { + let (mut ctx, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, - questions(), + vec![question_opinion_range(false)], 2, 100, String::from("Multiple questions test!"), - tags, + tags(), None, None, ); @@ -522,12 +533,7 @@ mod tests { false, alice(), poll_id, - vec![ - Some(Answer::YesNo(true)), - None, - Some(Answer::OpinionScale(5)), - None, - ], + vec![Some(Answer::OpinionRange(5))], ); assert!(res.is_ok()); ctx.predecessor_account_id = bob(); @@ -537,7 +543,7 @@ mod tests { false, bob(), poll_id, - vec![None, None, Some(Answer::OpinionScale(10)), None], + vec![Some(Answer::OpinionRange(10))], ); assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); @@ -547,7 +553,7 @@ mod tests { false, charlie(), poll_id, - vec![None, None, Some(Answer::OpinionScale(2)), None], + vec![Some(Answer::OpinionRange(2))], ); assert!(res.is_ok()); let results = ctr.results(poll_id); @@ -555,27 +561,24 @@ mod tests { results, Results { status: Status::Active, - number_of_participants: 3, - results: vec![ - PollResult::YesNo((1, 0)), - PollResult::TextChoices(vec![0, 0, 0]), - PollResult::OpinionScale(OpinionScaleResult { sum: 17, num: 3 }), - PollResult::TextAnswer - ] + participants: 3, + results: vec![PollResult::OpinionRange(OpinionRangeResult { + sum: 17, + num: 3 + }),] } ) } #[test] fn text_chocies_flow() { - let tags = vec![String::from("tag1"), String::from("tag2")]; - let (mut ctx, mut ctr) = setup(&admin()); + let (mut ctx, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, - questions(), + vec![question_text_choices(true)], 2, 100, String::from("Hello, world!"), - tags, + tags(), None, None, ); @@ -587,12 +590,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![ - None, - Some(Answer::TextChoices(vec![true, false, false])), - None, - None, - ], + vec![Some(Answer::TextChoices(vec![true, false, false]))], ); assert!(res.is_ok()); ctx.predecessor_account_id = bob(); @@ -602,12 +600,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![ - None, - Some(Answer::TextChoices(vec![true, false, false])), - None, - None, - ], + vec![Some(Answer::TextChoices(vec![true, false, false]))], ); assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); @@ -617,12 +610,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![ - None, - Some(Answer::TextChoices(vec![false, true, false])), - None, - None, - ], + vec![Some(Answer::TextChoices(vec![false, true, false]))], ); assert!(res.is_ok()); let results = ctr.results(poll_id); @@ -630,28 +618,22 @@ mod tests { results, Results { status: Status::Active, - number_of_participants: 3, - results: vec![ - PollResult::YesNo((0, 0)), - PollResult::TextChoices(vec![2, 1, 0]), - PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer - ] + participants: 3, + results: vec![PollResult::TextChoices(vec![2, 1, 0]),] } ) } #[test] fn text_answers_flow() { - let tags = vec![String::from("tag1"), String::from("tag2")]; - let (mut ctx, mut ctr) = setup(&admin()); + let (mut ctx, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, - questions(), + vec![question_text_answers(true)], 2, 100, String::from("Hello, world!"), - tags, + tags(), None, None, ); @@ -666,7 +648,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![None, None, None, Some(Answer::TextAnswer(answer1.clone()))], + vec![Some(Answer::TextAnswer(answer1.clone()))], ); assert!(res.is_ok()); ctx.predecessor_account_id = bob(); @@ -676,7 +658,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![None, None, None, Some(Answer::TextAnswer(answer2.clone()))], + vec![Some(Answer::TextAnswer(answer2.clone()))], ); assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); @@ -686,7 +668,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![None, None, None, Some(Answer::TextAnswer(answer3.clone()))], + vec![Some(Answer::TextAnswer(answer3.clone()))], ); assert!(res.is_ok()); let results = ctr.results(poll_id); @@ -694,17 +676,30 @@ mod tests { results, Results { status: Status::Active, - number_of_participants: 3, - results: vec![ - PollResult::YesNo((0, 0)), - PollResult::TextChoices(vec![0, 0, 0]), - PollResult::OpinionScale(OpinionScaleResult { sum: 0, num: 0 }), - PollResult::TextAnswer - ] + participants: 3, + results: vec![PollResult::TextAnswer] } ); - let text_answers = ctr.result_text_answers(poll_id, 3, 0); + let text_answers = ctr.result_text_answers(poll_id, 0, 0); assert!(text_answers.0); assert_eq!(text_answers.1, vec![answer1, answer2, answer3]) } + + #[test] + fn result_text_answers() { + let (mut ctx, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_text_answers(true)], + 2, + 100, + String::from("Hello, world!"), + tags(), + None, + None, + ); + mk_batch_text_answers(&mut ctr, alice(), poll_id, 50); + let text_answers = ctr._result_text_answers(poll_id, 0, 0, 30); + assert!(!text_answers.0); + } } diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index dcc60bd6..5284341a 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -11,7 +11,7 @@ pub enum Answer { YesNo(bool), TextChoices(Vec), // should respect the min_choices, max_choices PictureChoices(Vec), // should respect the min_choices, max_choices - OpinionScale(u8), // should be a number between 0 and 10 + OpinionRange(u8), // should be a number between 0 and 10 TextAnswer(String), } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] @@ -21,13 +21,13 @@ pub enum PollResult { YesNo((u32, u32)), // yes, no TextChoices(Vec), // should respect the min_choices, max_choices PictureChoices(Vec), // should respect the min_choices, max_choices - OpinionScale(OpinionScaleResult), // mean value + OpinionRange(OpinionRangeResult), // mean value TextAnswer, // indicates whether the question exist or not, the answers are stored in a different struct called `TextAnswers` } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] -pub struct OpinionScaleResult { +pub struct OpinionRangeResult { pub sum: u64, pub num: u64, } @@ -64,7 +64,7 @@ pub struct Poll { #[serde(crate = "near_sdk::serde")] pub struct PollResponse { answers: Vec<(usize, Answer)>, // question_id, answer - created_at: u64, // time in milliseconds + created_at: u64, // time in milliseconds } #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] @@ -72,16 +72,10 @@ pub struct PollResponse { #[serde(crate = "near_sdk::serde")] pub struct Results { pub status: Status, - pub participants: u64, // number of participants + pub participants: u64, // number of participants pub results: Vec, // question_id, result (sum of yes etc.) } -#[derive(BorshSerialize, BorshDeserialize, Serialize, Clone)] -#[serde(crate = "near_sdk::serde")] -pub struct TextAnswers { - pub answers: Vec, // question_id, list of answers -} - #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize, Clone)] #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] From 35af0ea18f5f059319873a22a29678a14b09aa2c Mon Sep 17 00:00:00 2001 From: sczembor Date: Fri, 11 Aug 2023 19:05:07 +0200 Subject: [PATCH 15/42] apply code review suggestions --- contracts/easy-poll/src/errors.rs | 6 ++++++ contracts/easy-poll/src/lib.rs | 12 +++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs index 4724ece7..18e52fad 100644 --- a/contracts/easy-poll/src/errors.rs +++ b/contracts/easy-poll/src/errors.rs @@ -10,6 +10,8 @@ pub enum PollError { NotFound, NotActive, OpinionRange, + WrongAnswer, + IncorrectAnswerVector, } impl FunctionError for PollError { @@ -22,6 +24,10 @@ impl FunctionError for PollError { PollError::NotFound => panic_str("poll not found"), PollError::NotActive => panic_str("poll is not active"), PollError::OpinionRange => panic_str("opinion must be between 0 and 10"), + PollError::WrongAnswer => { + panic_str("answer provied does not match the expected question") + }, + PollError::IncorrectAnswerVector => panic_str("The answer vector provided is incorrect and does not match the questions in the poll"), } } } diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index fdce50ac..5238bc9d 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -208,9 +208,12 @@ impl Contract { return Err(PollError::NoSBTs); } let questions: Vec = self.polls.get(&poll_id).unwrap().questions; + if questions.len() != answers.len() { + return Err(PollError::IncorrectAnswerVector); + } let mut unwrapped_answers: Vec = Vec::new(); let mut poll_results = self.results.get(&poll_id).unwrap(); - // let mut results = poll_results.results; + for i in 0..questions.len() { if questions[i].required && answers[i].is_none() { return Err(PollError::RequiredAnswer); @@ -252,11 +255,14 @@ impl Contract { }); } (Some(Answer::TextAnswer(answer)), _) => { - let mut answers = self.text_answers.get(&(poll_id, i)).expect("not found"); + let mut answers = self + .text_answers + .get(&(poll_id, i)) + .expect(&format!("question not found for index {:?}", i)); answers.push(answer); self.text_answers.insert(&(poll_id, i), &answers); } - (_, _) => (), + (_, _) => return Err(PollError::WrongAnswer), } if answers[i].is_some() { unwrapped_answers.push(answers[i].clone().unwrap()); From 791eca9d3bae6ef4e2c76796cf626382a7def062 Mon Sep 17 00:00:00 2001 From: sczembor Date: Mon, 14 Aug 2023 14:17:29 +0200 Subject: [PATCH 16/42] add more unit tests; remove unused structs --- contracts/easy-poll/src/lib.rs | 335 ++++++++++++++++++++++++++++- contracts/easy-poll/src/storage.rs | 8 +- 2 files changed, 330 insertions(+), 13 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 5238bc9d..792e3faa 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -88,11 +88,11 @@ impl Contract { .expect("poll not found") .questions .get(question) - .expect("question not type `TextAnswer`"); + .expect("question not found"); let text_answers = self .text_answers .get(&(poll_id, question)) - .expect("no answer found"); + .expect("question not type `TextAnswer`"); let to_return; let mut finished = false; if from_answer + limit > text_answers.len() as usize { @@ -291,7 +291,7 @@ impl Contract { None => return Err(PollError::NotFound), }; let current_timestamp = env::block_timestamp_ms(); - if poll.starts_at < current_timestamp || poll.ends_at > current_timestamp { + if poll.starts_at > current_timestamp || poll.ends_at < current_timestamp { return Err(PollError::NotActive); } Ok(()) @@ -349,7 +349,8 @@ mod tests { use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, VMContext}; use crate::{ - Answer, Contract, OpinionRangeResult, PollId, PollResult, Question, Results, Status, + Answer, Contract, OpinionRangeResult, PollError, PollId, PollResult, Question, Results, + Status, RESPOND_COST, }; const MILI_SECOND: u64 = 1000000; // nanoseconds @@ -463,6 +464,187 @@ mod tests { return (ctx, ctr); } + #[test] + #[should_panic(expected = "poll start must be in the future")] + fn create_poll_wrong_time() { + let (_, mut ctr) = setup(&alice()); + ctr.create_poll( + false, + vec![question_yes_no(true)], + 1, + 100, + String::from("Hello, world!"), + tags(), + None, + None, + ); + } + + #[test] + #[should_panic(expected = "respond not found")] + fn my_respond_not_found() { + let (_, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_yes_no(true)], + 2, + 100, + String::from("Hello, world!"), + tags(), + None, + None, + ); + ctr.my_respond(poll_id); + } + + #[test] + fn my_respond() { + let (mut ctx, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_yes_no(true)], + 2, + 100, + String::from("Hello, world!"), + tags(), + None, + None, + ); + ctx.block_timestamp = MILI_SECOND * 3; + testing_env!(ctx.clone()); + let res = ctr.on_human_verifed( + vec![], + false, + ctx.predecessor_account_id, + poll_id, + vec![Some(Answer::YesNo(true))], + ); + assert!(res.is_ok()); + let res = ctr.my_respond(poll_id); + assert_eq!(res, vec![Answer::YesNo(true)]) + } + + #[test] + #[should_panic(expected = "poll not found")] + fn results_poll_not_found() { + let (_, ctr) = setup(&alice()); + ctr.results(1); + } + + #[test] + fn results() { + let (_, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_yes_no(true)], + 2, + 100, + String::from("Hello, world!"), + tags(), + None, + None, + ); + let res = ctr.results(poll_id); + let expected = Results { + status: Status::NotStarted, + participants: 0, + results: vec![PollResult::YesNo((0, 0))], + }; + assert_eq!(res, expected); + } + + #[test] + #[should_panic(expected = "poll not found")] + fn result_text_answers_poll_not_found() { + let (_, ctr) = setup(&alice()); + ctr.result_text_answers(0, 0, 0); + } + + #[test] + #[should_panic(expected = "question not found")] + fn result_text_answers_wrong_question() { + let (_, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_yes_no(true)], + 2, + 100, + String::from("Hello, world!"), + tags(), + None, + None, + ); + ctr.result_text_answers(poll_id, 1, 0); + } + + #[test] + #[should_panic(expected = "question not type `TextAnswer`")] + fn result_text_answers_wrong_type() { + let (_, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_yes_no(true)], + 2, + 100, + String::from("Hello, world!"), + tags(), + None, + None, + ); + ctr.result_text_answers(poll_id, 0, 0); + } + + #[test] + #[should_panic(expected = "attached_deposit not sufficient")] + fn respond_wrong_deposit() { + let (mut ctx, mut ctr) = setup(&alice()); + ctx.attached_deposit = RESPOND_COST - 1; + testing_env!(ctx); + let res = ctr.respond(0, vec![Some(Answer::YesNo(true))]); + assert!(res.is_err()); + } + + #[test] + fn respond_poll_not_active() { + let (mut ctx, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_yes_no(true)], + 2, + 100, + String::from("Hello, world!"), + tags(), + None, + None, + ); + ctx.attached_deposit = RESPOND_COST; + testing_env!(ctx.clone()); + // too early + match ctr.respond(poll_id, vec![Some(Answer::YesNo(true))]) { + Err(err) => { + println!("Received error: {:?}", err); + match err { + PollError::NotActive => println!("Expected error: PollError::NotActive"), + _ => panic!("Unexpected error: {:?}", err), + } + } + Ok(_) => panic!("Received Ok result, but expected an error"), + } + ctx.block_timestamp = MILI_SECOND * 101; + testing_env!(ctx); + // too late + match ctr.respond(poll_id, vec![Some(Answer::YesNo(true))]) { + Err(err) => { + println!("Received error: {:?}", err); + match err { + PollError::NotActive => println!("Expected error: PollError::NotActive"), + _ => panic!("Unexpected error: {:?}", err), + } + } + Ok(_) => panic!("Received Ok result, but expected an error"), + } + } + #[test] fn yes_no_flow() { let (mut ctx, mut ctr) = setup(&alice()); @@ -476,7 +658,6 @@ mod tests { None, None, ); - ctx.predecessor_account_id = alice(); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx.clone()); let mut res = ctr.on_human_verifed( @@ -518,6 +699,77 @@ mod tests { ) } + #[test] + fn opinion_range_out_of_range() { + let (mut ctx, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_opinion_range(false)], + 2, + 100, + String::from("Multiple questions test!"), + tags(), + None, + None, + ); + ctx.block_timestamp = MILI_SECOND * 3; + testing_env!(ctx); + match ctr.on_human_verifed( + vec![], + false, + alice(), + poll_id, + vec![Some(Answer::OpinionRange(11))], + ) { + Err(err) => { + println!("Received error: {:?}", err); + match err { + PollError::OpinionRange => println!("Expected error: PollError::OpinionRange"), + _ => panic!("Unexpected error: {:?}", err), + } + } + Ok(_) => panic!("Received Ok result, but expected an error"), + } + } + + #[test] + fn respond_wrong_answer_vector() { + let (mut ctx, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + false, + vec![question_opinion_range(false)], + 2, + 100, + String::from("Multiple questions test!"), + tags(), + None, + None, + ); + ctx.block_timestamp = MILI_SECOND * 3; + testing_env!(ctx); + match ctr.on_human_verifed( + vec![], + false, + alice(), + poll_id, + vec![ + Some(Answer::OpinionRange(10)), + Some(Answer::OpinionRange(10)), + ], + ) { + Err(err) => { + println!("Received error: {:?}", err); + match err { + PollError::IncorrectAnswerVector => { + println!("Expected error: PollError::IncorrectAnswerVector") + } + _ => panic!("Unexpected error: {:?}", err), + } + } + Ok(_) => panic!("Received Ok result, but expected an error"), + } + } + #[test] fn opinion_range_flow() { let (mut ctx, mut ctr) = setup(&alice()); @@ -693,7 +945,7 @@ mod tests { #[test] fn result_text_answers() { - let (mut ctx, mut ctr) = setup(&alice()); + let (_, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, vec![question_text_answers(true)], @@ -705,7 +957,78 @@ mod tests { None, ); mk_batch_text_answers(&mut ctr, alice(), poll_id, 50); + // depending on the lenght of the answers the limit decreases rappidly let text_answers = ctr._result_text_answers(poll_id, 0, 0, 30); assert!(!text_answers.0); } + + #[test] + fn respond_iah_only_not_human() { + let (mut ctx, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + true, + vec![question_opinion_range(false)], + 2, + 100, + String::from("Multiple questions test!"), + tags(), + None, + None, + ); + ctx.block_timestamp = MILI_SECOND * 3; + testing_env!(ctx); + match ctr.on_human_verifed( + vec![], + true, + alice(), + poll_id, + vec![Some(Answer::OpinionRange(10))], + ) { + Err(err) => { + println!("Received error: {:?}", err); + match err { + PollError::NoSBTs => { + println!("Expected error: PollError::NoSBTs") + } + _ => panic!("Unexpected error: {:?}", err), + } + } + Ok(_) => panic!("Received Ok result, but expected an error"), + } + } + + #[test] + fn respond_required_answer_not_provided() { + let (mut ctx, mut ctr) = setup(&alice()); + let poll_id = ctr.create_poll( + true, + vec![question_opinion_range(false), question_opinion_range(true)], + 2, + 100, + String::from("Multiple questions test!"), + tags(), + None, + None, + ); + ctx.block_timestamp = MILI_SECOND * 3; + testing_env!(ctx); + match ctr.on_human_verifed( + vec![], + false, + alice(), + poll_id, + vec![Some(Answer::OpinionRange(10)), None], + ) { + Err(err) => { + println!("Received error: {:?}", err); + match err { + PollError::RequiredAnswer => { + println!("Expected error: PollError::RequiredAnswer") + } + _ => panic!("Unexpected error: {:?}", err), + } + } + Ok(_) => panic!("Received Ok result, but expected an error"), + } + } } diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 5284341a..374ca2c0 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -6,6 +6,7 @@ pub type PollId = u64; /// Helper structure for keys of the persistent collections. #[derive(BorshSerialize, BorshDeserialize, Deserialize, Serialize, Clone)] +#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] pub enum Answer { YesNo(bool), @@ -60,13 +61,6 @@ pub struct Poll { pub created_at: u64, // time in milliseconds, should be assigned by the smart contract not a user. } -#[derive(Deserialize, Serialize)] -#[serde(crate = "near_sdk::serde")] -pub struct PollResponse { - answers: Vec<(usize, Answer)>, // question_id, answer - created_at: u64, // time in milliseconds -} - #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] From eebbe4aaab9324070d78c92bafc8481867f5006a Mon Sep 17 00:00:00 2001 From: sczembor Date: Wed, 30 Aug 2023 13:11:33 +0200 Subject: [PATCH 17/42] apply code review suggestions --- contracts/easy-poll/src/lib.rs | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 792e3faa..e6cd46b9 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -23,7 +23,7 @@ pub struct Contract { /// map of all results summarized pub results: LookupMap, /// map of all answers, (poll, user) -> vec of answers - pub answers: UnorderedMap<(PollId, AccountId), Vec>, + pub answers: UnorderedMap<(PollId, AccountId), Vec>>, /// text answers are stored in a separate map pub text_answers: LookupMap<(PollId, usize), Vector>, /// SBT registry. @@ -51,7 +51,7 @@ impl Contract { **********/ /// Returns caller response to the specified poll - pub fn my_respond(&self, poll_id: PollId) -> Vec { + pub fn my_respond(&self, poll_id: PollId) -> Vec> { let caller = env::predecessor_account_id(); self.answers .get(&(poll_id, caller)) @@ -207,12 +207,12 @@ impl Contract { if iah_only && tokens.is_empty() { return Err(PollError::NoSBTs); } - let questions: Vec = self.polls.get(&poll_id).unwrap().questions; + let questions: Vec = self.polls.get(&poll_id).expect("poll not found").questions; if questions.len() != answers.len() { return Err(PollError::IncorrectAnswerVector); } - let mut unwrapped_answers: Vec = Vec::new(); - let mut poll_results = self.results.get(&poll_id).unwrap(); + let mut unwrapped_answers: Vec> = Vec::new(); + let mut poll_results = self.results.get(&poll_id).expect("results not found"); for i in 0..questions.len() { if questions[i].required && answers[i].is_none() { @@ -262,10 +262,13 @@ impl Contract { answers.push(answer); self.text_answers.insert(&(poll_id, i), &answers); } + (None, _) => { + unwrapped_answers.push(None); + } (_, _) => return Err(PollError::WrongAnswer), } if answers[i].is_some() { - unwrapped_answers.push(answers[i].clone().unwrap()); + unwrapped_answers.push(Some(answers[i].clone().unwrap())); } } let mut answers = self @@ -502,7 +505,7 @@ mod tests { let (mut ctx, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, - vec![question_yes_no(true)], + vec![question_yes_no(false), question_yes_no(true)], 2, 100, String::from("Hello, world!"), @@ -517,11 +520,11 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![Some(Answer::YesNo(true))], + vec![None, Some(Answer::YesNo(true))], ); assert!(res.is_ok()); let res = ctr.my_respond(poll_id); - assert_eq!(res, vec![Answer::YesNo(true)]) + assert_eq!(res, vec![None, Some(Answer::YesNo(true))]) } #[test] From 70c13b38156d3121ee6d5326edcd06a1af9c33c6 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Wed, 30 Aug 2023 16:58:45 +0200 Subject: [PATCH 18/42] Update contracts/easy-poll/src/ext.rs Co-authored-by: Amit Yadav --- contracts/easy-poll/src/ext.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/contracts/easy-poll/src/ext.rs b/contracts/easy-poll/src/ext.rs index 8c90d3b7..a8468d90 100644 --- a/contracts/easy-poll/src/ext.rs +++ b/contracts/easy-poll/src/ext.rs @@ -5,6 +5,5 @@ use sbt::TokenId; #[ext_contract(ext_registry)] trait ExtRegistry { // queries - fn is_human(&self, account: AccountId) -> Vec<(AccountId, Vec)>; } From 41433b54607e69341dede124ce8205f40135acc5 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:26:33 +0200 Subject: [PATCH 19/42] Update contracts/easy-poll/src/storage.rs Co-authored-by: Amit Yadav --- contracts/easy-poll/src/storage.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 374ca2c0..fc8ce8ff 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -15,6 +15,7 @@ pub enum Answer { OpinionRange(u8), // should be a number between 0 and 10 TextAnswer(String), } + #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] From f93aad58358118360cad7f9ee7facbd70a02cad3 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:26:42 +0200 Subject: [PATCH 20/42] Update contracts/easy-poll/src/storage.rs Co-authored-by: Amit Yadav --- contracts/easy-poll/src/storage.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index fc8ce8ff..b3d79bc5 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -26,6 +26,7 @@ pub enum PollResult { OpinionRange(OpinionRangeResult), // mean value TextAnswer, // indicates whether the question exist or not, the answers are stored in a different struct called `TextAnswers` } + #[derive(BorshSerialize, BorshDeserialize, Serialize, Deserialize)] #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] #[serde(crate = "near_sdk::serde")] From 4952829384d629bb3a2f987288cf997c6b4465f1 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:28:07 +0200 Subject: [PATCH 21/42] Update contracts/easy-poll/src/lib.rs Co-authored-by: Amit Yadav --- contracts/easy-poll/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 792e3faa..1592597d 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -73,7 +73,7 @@ impl Contract { self._result_text_answers(poll_id, question, from_answer, 20) } - ///Returns a fixed value of answers + /// Returns a fixed value of answers // Function must be called until true is returned -> meaning all the answers were returned // `question` must be an index of the text question in the poll pub fn _result_text_answers( From 7514834faac281c68e292246a9d7969fe897e6c2 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Thu, 31 Aug 2023 15:28:20 +0200 Subject: [PATCH 22/42] Update contracts/easy-poll/src/lib.rs Co-authored-by: Amit Yadav --- contracts/easy-poll/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 1592597d..0554bbd1 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -374,6 +374,7 @@ mod tests { fn tags() -> Vec { vec![String::from("tag1"), String::from("tag2")] } + fn question_text_answers(required: bool) -> Question { Question { question_type: Answer::TextAnswer(String::from("")), From 1ea68898db5c59b64e2d97f84c4d04a0417552e2 Mon Sep 17 00:00:00 2001 From: sczembor Date: Thu, 31 Aug 2023 15:53:43 +0200 Subject: [PATCH 23/42] apply code review suggestions --- contracts/easy-poll/src/lib.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index cdb191cc..0fbfab48 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -104,6 +104,10 @@ impl Contract { (finished, to_return) } + /********** + * TRANSACTIONS + **********/ + /// User can update the poll if starts_at > now /// it panics if /// - user tries to create an invalid poll @@ -144,7 +148,8 @@ impl Contract { poll_id } - /// user can change his answer when the poll is still active. + /// user can change his answer when the poll is still active + // TODO: currently we do not allow users to change the answer /// it panics if /// - poll not found /// - poll not active @@ -171,7 +176,10 @@ impl Contract { // TODO: I think we should add a option for the poll creator to choose whether changing // the answers while the poll is active is allowed or not self.assert_answered(poll_id, &caller); - let poll = self.polls.get(&poll_id).unwrap(); + let poll = match self.polls.get(&poll_id) { + None => return Err(PollError::NotFound), + Some(poll) => poll, + }; // if iah calls the registry to verify the iah sbt if poll.iah_only { ext_registry::ext(self.registry.clone()) From f8b137a013629250e0b37b735e312adb84722c8d Mon Sep 17 00:00:00 2001 From: sczembor Date: Thu, 31 Aug 2023 15:55:36 +0200 Subject: [PATCH 24/42] add comment' --- contracts/easy-poll/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 0fbfab48..e6dc5f0b 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -70,6 +70,7 @@ impl Contract { question: usize, from_answer: usize, ) -> (bool, Vec) { + // We cannot return more than 20 due to gas limit per txn. self._result_text_answers(poll_id, question, from_answer, 20) } From 3f2ac0530651cffa92e88a1766befb757e2bd2c3 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:54:54 +0200 Subject: [PATCH 25/42] Update contracts/easy-poll/src/lib.rs Co-authored-by: Robert Zaremba --- contracts/easy-poll/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index e6dc5f0b..d8c43e99 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -51,7 +51,7 @@ impl Contract { **********/ /// Returns caller response to the specified poll - pub fn my_respond(&self, poll_id: PollId) -> Vec> { + pub fn my_response(&self, poll_id: PollId) -> Vec> { let caller = env::predecessor_account_id(); self.answers .get(&(poll_id, caller)) From b2d30452623c5d5655ec5a0d1c743ac7df1ec2e2 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Fri, 8 Sep 2023 15:55:07 +0200 Subject: [PATCH 26/42] Update contracts/easy-poll/src/lib.rs Co-authored-by: Robert Zaremba --- contracts/easy-poll/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index d8c43e99..37352932 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -121,8 +121,8 @@ impl Contract { ends_at: u64, title: String, tags: Vec, - description: Option, - link: Option, + description: String, + link: String, ) -> PollId { let created_at = env::block_timestamp_ms(); require!( From 12c5af885cfccd275ba20b52712673d09aeb6ac9 Mon Sep 17 00:00:00 2001 From: sczembor Date: Fri, 8 Sep 2023 16:59:19 +0200 Subject: [PATCH 27/42] add events --- contracts/easy-poll/src/events.rs | 46 +++++++++++ contracts/easy-poll/src/lib.rs | 119 +++++++++++++++++++---------- contracts/easy-poll/src/storage.rs | 4 +- 3 files changed, 128 insertions(+), 41 deletions(-) create mode 100644 contracts/easy-poll/src/events.rs diff --git a/contracts/easy-poll/src/events.rs b/contracts/easy-poll/src/events.rs new file mode 100644 index 00000000..c7f23c42 --- /dev/null +++ b/contracts/easy-poll/src/events.rs @@ -0,0 +1,46 @@ +use near_sdk::serde::Serialize; +use serde_json::json; + +use sbt::{EventPayload, NearEvent}; + +use crate::PollId; + +fn emit_event(event: EventPayload) { + NearEvent { + standard: "ndc-easy-polls", + version: "0.0.1", + event, + } + .emit(); +} + +pub(crate) fn emit_create_poll(poll_id: PollId) { + emit_event(EventPayload { + event: "create_poll", + data: json!({ "poll_id": poll_id }), + }); +} + +pub(crate) fn emit_respond(poll_id: PollId) { + emit_event(EventPayload { + event: "respond", + data: json!({ "poll_id": poll_id }), + }); +} + +#[cfg(test)] +mod unit_tests { + use near_sdk::test_utils; + + use super::*; + + #[test] + fn log_vote() { + let expected1 = r#"EVENT_JSON:{"standard":"ndc-easy-polls","version":"0.0.1","event":"create_poll","data":{"poll_id":21}}"#; + let expected2 = r#"EVENT_JSON:{"standard":"ndc-easy-polls","version":"0.0.1","event":"respond","data":{"poll_id":22}}"#; + emit_create_poll(21); + assert_eq!(vec![expected1], test_utils::get_logs()); + emit_respond(22); + assert_eq!(vec![expected1, expected2], test_utils::get_logs()); + } +} diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 37352932..55eeb785 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -1,4 +1,6 @@ pub use crate::errors::PollError; +use crate::events::emit_create_poll; +use crate::events::emit_respond; pub use crate::ext::*; pub use crate::storage::*; use cost::MILI_NEAR; @@ -9,6 +11,7 @@ use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; use near_sdk::{Balance, Gas}; mod errors; +mod events; mod ext; mod storage; @@ -42,7 +45,7 @@ impl Contract { answers: UnorderedMap::new(StorageKey::Answers), text_answers: LookupMap::new(StorageKey::TextAnswers), registry, - next_poll_id: 0, + next_poll_id: 1, } } @@ -113,6 +116,7 @@ impl Contract { /// it panics if /// - user tries to create an invalid poll /// - if poll aready exists and starts_at < now + /// emits create_poll event pub fn create_poll( &mut self, iah_only: bool, @@ -146,6 +150,7 @@ impl Contract { created_at, }, ); + emit_create_poll(poll_id); poll_id } @@ -155,7 +160,8 @@ impl Contract { /// - poll not found /// - poll not active /// - poll.verified_humans_only is true, and user is not verified on IAH - // - user tries to vote with an invalid answer to a question + /// - user tries to vote with an invalid answer to a question + /// emits repond event #[payable] #[handle_result] pub fn respond( @@ -290,6 +296,7 @@ impl Contract { poll_results.status = Status::Active; poll_results.participants += 1; self.results.insert(&poll_id, &poll_results); + emit_respond(poll_id); Ok(()) } @@ -358,7 +365,10 @@ impl Contract { #[cfg(test)] mod tests { - use near_sdk::{test_utils::VMContextBuilder, testing_env, AccountId, VMContext}; + use near_sdk::{ + test_utils::{self, VMContextBuilder}, + testing_env, AccountId, VMContext, + }; use crate::{ Answer, Contract, OpinionRangeResult, PollError, PollId, PollResult, Question, Results, @@ -488,14 +498,32 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); } + #[test] + fn create_poll() { + let (_, mut ctr) = setup(&alice()); + ctr.create_poll( + false, + vec![question_yes_no(true)], + 2, + 100, + String::from("Hello, world!"), + tags(), + String::from(""), + String::from(""), + ); + let expected_event = r#"EVENT_JSON:{"standard":"ndc-easy-polls","version":"0.0.1","event":"create_poll","data":{"poll_id":1}}"#; + assert!(test_utils::get_logs().len() == 1); + assert_eq!(test_utils::get_logs()[0], expected_event); + } + #[test] #[should_panic(expected = "respond not found")] - fn my_respond_not_found() { + fn my_response_not_found() { let (_, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, @@ -504,14 +532,14 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); - ctr.my_respond(poll_id); + ctr.my_response(poll_id); } #[test] - fn my_respond() { + fn my_response() { let (mut ctx, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, @@ -520,8 +548,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx.clone()); @@ -533,7 +561,7 @@ mod tests { vec![None, Some(Answer::YesNo(true))], ); assert!(res.is_ok()); - let res = ctr.my_respond(poll_id); + let res = ctr.my_response(poll_id); assert_eq!(res, vec![None, Some(Answer::YesNo(true))]) } @@ -554,8 +582,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); let res = ctr.results(poll_id); let expected = Results { @@ -584,8 +612,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctr.result_text_answers(poll_id, 1, 0); } @@ -601,8 +629,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctr.result_text_answers(poll_id, 0, 0); } @@ -627,8 +655,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.attached_deposit = RESPOND_COST; testing_env!(ctx.clone()); @@ -668,8 +696,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx.clone()); @@ -681,6 +709,11 @@ mod tests { vec![Some(Answer::YesNo(true))], ); assert!(res.is_ok()); + + let expected_event = r#"EVENT_JSON:{"standard":"ndc-easy-polls","version":"0.0.1","event":"respond","data":{"poll_id":1}}"#; + assert!(test_utils::get_logs().len() == 1); + assert_eq!(test_utils::get_logs()[0], expected_event); + ctx.predecessor_account_id = bob(); testing_env!(ctx.clone()); res = ctr.on_human_verifed( @@ -691,6 +724,10 @@ mod tests { vec![Some(Answer::YesNo(true))], ); assert!(res.is_ok()); + + assert!(test_utils::get_logs().len() == 1); + assert_eq!(test_utils::get_logs()[0], expected_event); + ctx.predecessor_account_id = charlie(); testing_env!(ctx.clone()); res = ctr.on_human_verifed( @@ -701,6 +738,10 @@ mod tests { vec![Some(Answer::YesNo(false))], ); assert!(res.is_ok()); + + assert!(test_utils::get_logs().len() == 1); + assert_eq!(test_utils::get_logs()[0], expected_event); + let results = ctr.results(poll_id); assert_eq!( results, @@ -722,8 +763,8 @@ mod tests { 100, String::from("Multiple questions test!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx); @@ -755,8 +796,8 @@ mod tests { 100, String::from("Multiple questions test!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx); @@ -793,8 +834,8 @@ mod tests { 100, String::from("Multiple questions test!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.predecessor_account_id = alice(); ctx.block_timestamp = MILI_SECOND * 3; @@ -850,8 +891,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.predecessor_account_id = alice(); ctx.block_timestamp = MILI_SECOND * 3; @@ -905,8 +946,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.predecessor_account_id = alice(); ctx.block_timestamp = MILI_SECOND * 3; @@ -966,8 +1007,8 @@ mod tests { 100, String::from("Hello, world!"), tags(), - None, - None, + String::from(""), + String::from(""), ); mk_batch_text_answers(&mut ctr, alice(), poll_id, 50); // depending on the lenght of the answers the limit decreases rappidly @@ -985,8 +1026,8 @@ mod tests { 100, String::from("Multiple questions test!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx); @@ -1020,8 +1061,8 @@ mod tests { 100, String::from("Multiple questions test!"), tags(), - None, - None, + String::from(""), + String::from(""), ); ctx.block_timestamp = MILI_SECOND * 3; testing_env!(ctx); diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index b3d79bc5..714cfb87 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -58,8 +58,8 @@ pub struct Poll { pub ends_at: u64, // required, time in milliseconds pub title: String, // required pub tags: Vec, // can be an empty vector - pub description: Option, // optional - pub link: Option, // optional + pub description: String, // can be an empty string + pub link: String, // can be an empty string pub created_at: u64, // time in milliseconds, should be assigned by the smart contract not a user. } From 590c4f1c4775b13037331583ca22634c5f7c77a9 Mon Sep 17 00:00:00 2001 From: sczembor Date: Mon, 11 Sep 2023 15:37:48 +0200 Subject: [PATCH 28/42] apply code review suggestions --- contracts/easy-poll/src/errors.rs | 10 ++- contracts/easy-poll/src/lib.rs | 131 ++++++++++++++++++----------- contracts/easy-poll/src/storage.rs | 8 ++ 3 files changed, 94 insertions(+), 55 deletions(-) diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs index 18e52fad..28843fce 100644 --- a/contracts/easy-poll/src/errors.rs +++ b/contracts/easy-poll/src/errors.rs @@ -5,20 +5,21 @@ use near_sdk::FunctionError; #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq))] #[derive(Debug)] pub enum PollError { - RequiredAnswer, + RequiredAnswer(usize), NoSBTs, NotFound, NotActive, OpinionRange, WrongAnswer, IncorrectAnswerVector, + AlredyAnswered, } impl FunctionError for PollError { fn panic(&self) -> ! { match self { - PollError::RequiredAnswer => { - panic_str("Answer to a required question was not provided") + PollError::RequiredAnswer(index) => { + panic_str(&format!("Answer to a required question index={} was not provided",index)) } PollError::NoSBTs => panic_str("voter is not a verified human"), PollError::NotFound => panic_str("poll not found"), @@ -27,7 +28,8 @@ impl FunctionError for PollError { PollError::WrongAnswer => { panic_str("answer provied does not match the expected question") }, - PollError::IncorrectAnswerVector => panic_str("The answer vector provided is incorrect and does not match the questions in the poll"), + PollError::IncorrectAnswerVector => panic_str("the answer vector provided is incorrect and does not match the questions in the poll"), + PollError::AlredyAnswered => panic_str("user has already answered") } } } diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 55eeb785..c9d7e306 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -53,17 +53,20 @@ impl Contract { * QUERIES **********/ + /// Returns the poll details. If poll not found returns None. + pub fn poll(&self, poll_id: PollId) -> Option { + self.polls.get(&poll_id) + } + /// Returns caller response to the specified poll - pub fn my_response(&self, poll_id: PollId) -> Vec> { + pub fn my_response(&self, poll_id: PollId) -> Option>> { let caller = env::predecessor_account_id(); - self.answers - .get(&(poll_id, caller)) - .expect("respond not found") + self.answers.get(&(poll_id, caller)) } - /// Returns poll results (except for text answers), if poll not found panics - pub fn results(&self, poll_id: u64) -> Results { - self.results.get(&poll_id).expect("poll not found") + /// Returns poll results (except for text answers), if poll not found returns None. + pub fn results(&self, poll_id: u64) -> Option { + self.results.get(&poll_id) } /// Returns text answers in rounds. Starts from the question id provided. Needs to be called until true is returned. @@ -72,7 +75,7 @@ impl Contract { poll_id: u64, question: usize, from_answer: usize, - ) -> (bool, Vec) { + ) -> TextResponse<(bool, Vec)> { // We cannot return more than 20 due to gas limit per txn. self._result_text_answers(poll_id, question, from_answer, 20) } @@ -86,17 +89,21 @@ impl Contract { question: usize, from_answer: usize, limit: usize, - ) -> (bool, Vec) { - self.polls - .get(&poll_id) - .expect("poll not found") - .questions - .get(question) - .expect("question not found"); - let text_answers = self - .text_answers - .get(&(poll_id, question)) - .expect("question not type `TextAnswer`"); + ) -> TextResponse<(bool, Vec)> { + let poll = match self.polls.get(&poll_id) { + Some(poll) => poll, + None => return TextResponse::PollNotFound, + }; + + match poll.questions.get(question) { + Some(questions) => questions, + None => return TextResponse::QuestionNotFound, + }; + + let text_answers = match self.text_answers.get(&(poll_id, question)) { + Some(text_answers) => text_answers, + None => return TextResponse::QuestionWrongType, + }; let to_return; let mut finished = false; if from_answer + limit > text_answers.len() as usize { @@ -105,7 +112,7 @@ impl Contract { } else { to_return = text_answers.to_vec()[from_answer..from_answer + limit].to_vec(); } - (finished, to_return) + TextResponse::Ok((finished, to_return)) } /********** @@ -182,7 +189,11 @@ impl Contract { // TODO: I think we should add a option for the poll creator to choose whether changing // the answers while the poll is active is allowed or not - self.assert_answered(poll_id, &caller); + match self.assert_answered(poll_id, &caller) { + Err(err) => return Err(err), + Ok(_) => (), + } + let poll = match self.polls.get(&poll_id) { None => return Err(PollError::NotFound), Some(poll) => poll, @@ -231,7 +242,7 @@ impl Contract { for i in 0..questions.len() { if questions[i].required && answers[i].is_none() { - return Err(PollError::RequiredAnswer); + return Err(PollError::RequiredAnswer(i)); } match (&answers[i], &poll_results.results[i]) { @@ -303,7 +314,7 @@ impl Contract { /********** * INTERNAL **********/ - #[handle_result] + fn assert_active(&self, poll_id: PollId) -> Result<(), PollError> { let poll = match self.polls.get(&poll_id) { Some(poll) => poll, @@ -316,11 +327,11 @@ impl Contract { Ok(()) } - fn assert_answered(&self, poll_id: PollId, caller: &AccountId) { - require!( - self.answers.get(&(poll_id, caller.clone())).is_none(), - format!("user: {} has already answered", caller) - ); + fn assert_answered(&self, poll_id: PollId, caller: &AccountId) -> Result<(), PollError> { + if self.answers.get(&(poll_id, caller.clone())).is_some() { + return Err(PollError::AlredyAnswered); + } + Ok(()) } fn initalize_results(&mut self, poll_id: PollId, questions: &Vec) { @@ -372,7 +383,7 @@ mod tests { use crate::{ Answer, Contract, OpinionRangeResult, PollError, PollId, PollResult, Question, Results, - Status, RESPOND_COST, + Status, TextResponse, RESPOND_COST, }; const MILI_SECOND: u64 = 1000000; // nanoseconds @@ -522,7 +533,6 @@ mod tests { } #[test] - #[should_panic(expected = "respond not found")] fn my_response_not_found() { let (_, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( @@ -535,7 +545,7 @@ mod tests { String::from(""), String::from(""), ); - ctr.my_response(poll_id); + assert!(ctr.my_response(poll_id).is_none()); } #[test] @@ -562,14 +572,13 @@ mod tests { ); assert!(res.is_ok()); let res = ctr.my_response(poll_id); - assert_eq!(res, vec![None, Some(Answer::YesNo(true))]) + assert_eq!(res.unwrap(), vec![None, Some(Answer::YesNo(true))]) } #[test] - #[should_panic(expected = "poll not found")] fn results_poll_not_found() { let (_, ctr) = setup(&alice()); - ctr.results(1); + assert!(ctr.results(1).is_none()); } #[test] @@ -591,19 +600,20 @@ mod tests { participants: 0, results: vec![PollResult::YesNo((0, 0))], }; - assert_eq!(res, expected); + assert_eq!(res.unwrap(), expected); } #[test] - #[should_panic(expected = "poll not found")] fn result_text_answers_poll_not_found() { let (_, ctr) = setup(&alice()); - ctr.result_text_answers(0, 0, 0); + match ctr.result_text_answers(0, 0, 0) { + TextResponse::PollNotFound => (), + other => panic!("Expected TextResponse::PollNotFound, but got {:?}", other), + }; } #[test] - #[should_panic(expected = "question not found")] - fn result_text_answers_wrong_question() { + fn result_text_answers_question_not_found() { let (_, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, @@ -615,12 +625,17 @@ mod tests { String::from(""), String::from(""), ); - ctr.result_text_answers(poll_id, 1, 0); + match ctr.result_text_answers(poll_id, 1, 0) { + TextResponse::QuestionNotFound => (), + other => panic!( + "Expected TextResponse::QuestionNotFound, but got {:?}", + other + ), + }; } #[test] - #[should_panic(expected = "question not type `TextAnswer`")] - fn result_text_answers_wrong_type() { + fn result_text_answers_question_wrong_type() { let (_, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, @@ -632,7 +647,13 @@ mod tests { String::from(""), String::from(""), ); - ctr.result_text_answers(poll_id, 0, 0); + match ctr.result_text_answers(poll_id, 0, 0) { + TextResponse::QuestionWrongType => (), + other => panic!( + "Expected TextResponse::QuestionWrongType, but got {:?}", + other + ), + }; } #[test] @@ -744,7 +765,7 @@ mod tests { let results = ctr.results(poll_id); assert_eq!( - results, + results.unwrap(), Results { status: Status::Active, participants: 3, @@ -870,7 +891,7 @@ mod tests { assert!(res.is_ok()); let results = ctr.results(poll_id); assert_eq!( - results, + results.unwrap(), Results { status: Status::Active, participants: 3, @@ -927,7 +948,7 @@ mod tests { assert!(res.is_ok()); let results = ctr.results(poll_id); assert_eq!( - results, + results.unwrap(), Results { status: Status::Active, participants: 3, @@ -985,7 +1006,7 @@ mod tests { assert!(res.is_ok()); let results = ctr.results(poll_id); assert_eq!( - results, + results.unwrap(), Results { status: Status::Active, participants: 3, @@ -993,8 +1014,10 @@ mod tests { } ); let text_answers = ctr.result_text_answers(poll_id, 0, 0); - assert!(text_answers.0); - assert_eq!(text_answers.1, vec![answer1, answer2, answer3]) + assert_eq!( + text_answers, + TextResponse::Ok((true, vec![answer1, answer2, answer3])) + ); } #[test] @@ -1013,7 +1036,13 @@ mod tests { mk_batch_text_answers(&mut ctr, alice(), poll_id, 50); // depending on the lenght of the answers the limit decreases rappidly let text_answers = ctr._result_text_answers(poll_id, 0, 0, 30); - assert!(!text_answers.0); + match text_answers { + TextResponse::Ok((false, _)) => {} + _ => panic!( + "Expected TextResponse::Ok with true, but got {:?}", + text_answers + ), + } } #[test] @@ -1076,7 +1105,7 @@ mod tests { Err(err) => { println!("Received error: {:?}", err); match err { - PollError::RequiredAnswer => { + PollError::RequiredAnswer(1) => { println!("Expected error: PollError::RequiredAnswer") } _ => panic!("Unexpected error: {:?}", err), diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 714cfb87..8fa8fc26 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -88,3 +88,11 @@ pub enum StorageKey { Answers, TextAnswers, } + +#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] +pub enum TextResponse { + Ok(T), + PollNotFound, + QuestionNotFound, + QuestionWrongType, +} From 6707620ee3559dff4560e8650286c604f075f9bf Mon Sep 17 00:00:00 2001 From: sczembor Date: Mon, 11 Sep 2023 15:55:01 +0200 Subject: [PATCH 29/42] add serialize, deserialize --- contracts/easy-poll/src/storage.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 8fa8fc26..02832b9d 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -89,7 +89,9 @@ pub enum StorageKey { TextAnswers, } +#[derive(Deserialize, Serialize)] #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] +#[serde(crate = "near_sdk::serde")] pub enum TextResponse { Ok(T), PollNotFound, From d5dd012492c959ef8b4361b8c7a3b123ba904a5a Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Mon, 11 Sep 2023 17:43:24 +0200 Subject: [PATCH 30/42] Apply suggestions from code review Co-authored-by: Robert Zaremba --- contracts/easy-poll/src/events.rs | 8 ++++---- contracts/easy-poll/src/lib.rs | 23 ++++++++--------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/contracts/easy-poll/src/events.rs b/contracts/easy-poll/src/events.rs index c7f23c42..f80c9ead 100644 --- a/contracts/easy-poll/src/events.rs +++ b/contracts/easy-poll/src/events.rs @@ -7,8 +7,8 @@ use crate::PollId; fn emit_event(event: EventPayload) { NearEvent { - standard: "ndc-easy-polls", - version: "0.0.1", + standard: "ndc-easy-poll", + version: "1.0.0", event, } .emit(); @@ -21,10 +21,10 @@ pub(crate) fn emit_create_poll(poll_id: PollId) { }); } -pub(crate) fn emit_respond(poll_id: PollId) { +pub(crate) fn emit_respond(poll_id: PollId, responded: AccountId) { emit_event(EventPayload { event: "respond", - data: json!({ "poll_id": poll_id }), + data: json!({ "poll_id": poll_id, "responder": responder }), }); } diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index c9d7e306..3412c507 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -27,7 +27,7 @@ pub struct Contract { pub results: LookupMap, /// map of all answers, (poll, user) -> vec of answers pub answers: UnorderedMap<(PollId, AccountId), Vec>>, - /// text answers are stored in a separate map + /// text answers are stored in a separate map. Key is a (pollId, question index). pub text_answers: LookupMap<(PollId, usize), Vector>, /// SBT registry. pub registry: AccountId, @@ -58,7 +58,7 @@ impl Contract { self.polls.get(&poll_id) } - /// Returns caller response to the specified poll + /// Returns caller response to the specified poll. It doesn't return text responses of the given poll ID. pub fn my_response(&self, poll_id: PollId) -> Option>> { let caller = env::predecessor_account_id(); self.answers.get(&(poll_id, caller)) @@ -70,7 +70,7 @@ impl Contract { } /// Returns text answers in rounds. Starts from the question id provided. Needs to be called until true is returned. - pub fn result_text_answers( + pub fn text_answers( &self, poll_id: u64, question: usize, @@ -89,7 +89,7 @@ impl Contract { question: usize, from_answer: usize, limit: usize, - ) -> TextResponse<(bool, Vec)> { + ) -> TextResponse<(Vec, bool)> { let poll = match self.polls.get(&poll_id) { Some(poll) => poll, None => return TextResponse::PollNotFound, @@ -112,7 +112,7 @@ impl Contract { } else { to_return = text_answers.to_vec()[from_answer..from_answer + limit].to_vec(); } - TextResponse::Ok((finished, to_return)) + TextResponse::Ok((to_return, finished)) } /********** @@ -138,7 +138,7 @@ impl Contract { let created_at = env::block_timestamp_ms(); require!( created_at < starts_at, - format!("poll start must be in the future") + "poll start must be in the future".to_string() ); let poll_id = self.next_poll_id; self.next_poll_id += 1; @@ -182,18 +182,11 @@ impl Contract { ); let caller = env::predecessor_account_id(); - match self.assert_active(poll_id) { - Err(err) => return Err(err), - Ok(_) => (), - }; + self.assert_active(poll_id)?; // TODO: I think we should add a option for the poll creator to choose whether changing // the answers while the poll is active is allowed or not - match self.assert_answered(poll_id, &caller) { - Err(err) => return Err(err), - Ok(_) => (), - } - + self.assert_answered(poll_id, &caller)?; let poll = match self.polls.get(&poll_id) { None => return Err(PollError::NotFound), Some(poll) => poll, From 56519f907850819eb05c67613e5470a1adc3a9f6 Mon Sep 17 00:00:00 2001 From: sczembor Date: Mon, 11 Sep 2023 20:48:11 +0200 Subject: [PATCH 31/42] apply code review suggestions --- contracts/easy-poll/src/errors.rs | 6 ++- contracts/easy-poll/src/events.rs | 16 +++--- contracts/easy-poll/src/lib.rs | 85 ++++++++++++++++++------------- 3 files changed, 64 insertions(+), 43 deletions(-) diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs index 28843fce..9f49eafd 100644 --- a/contracts/easy-poll/src/errors.rs +++ b/contracts/easy-poll/src/errors.rs @@ -1,6 +1,8 @@ use near_sdk::env::panic_str; use near_sdk::FunctionError; +use crate::MAX_TEXT_ANSWER_LEN; + /// Contract errors #[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq))] #[derive(Debug)] @@ -13,6 +15,7 @@ pub enum PollError { WrongAnswer, IncorrectAnswerVector, AlredyAnswered, + AnswerTooLong(usize), } impl FunctionError for PollError { @@ -29,7 +32,8 @@ impl FunctionError for PollError { panic_str("answer provied does not match the expected question") }, PollError::IncorrectAnswerVector => panic_str("the answer vector provided is incorrect and does not match the questions in the poll"), - PollError::AlredyAnswered => panic_str("user has already answered") + PollError::AlredyAnswered => panic_str("user has already answered"), + PollError::AnswerTooLong(len) => {panic_str(&format!("the answer too long, max_len:{}, got:{}", MAX_TEXT_ANSWER_LEN, len))} } } } diff --git a/contracts/easy-poll/src/events.rs b/contracts/easy-poll/src/events.rs index f80c9ead..eac36b51 100644 --- a/contracts/easy-poll/src/events.rs +++ b/contracts/easy-poll/src/events.rs @@ -1,4 +1,4 @@ -use near_sdk::serde::Serialize; +use near_sdk::{serde::Serialize, AccountId}; use serde_json::json; use sbt::{EventPayload, NearEvent}; @@ -21,7 +21,7 @@ pub(crate) fn emit_create_poll(poll_id: PollId) { }); } -pub(crate) fn emit_respond(poll_id: PollId, responded: AccountId) { +pub(crate) fn emit_respond(poll_id: PollId, responder: AccountId) { emit_event(EventPayload { event: "respond", data: json!({ "poll_id": poll_id, "responder": responder }), @@ -30,17 +30,21 @@ pub(crate) fn emit_respond(poll_id: PollId, responded: AccountId) { #[cfg(test)] mod unit_tests { - use near_sdk::test_utils; + use near_sdk::{test_utils, AccountId}; use super::*; + fn acc(idx: u8) -> AccountId { + AccountId::new_unchecked(format!("user-{}.near", idx)) + } + #[test] fn log_vote() { - let expected1 = r#"EVENT_JSON:{"standard":"ndc-easy-polls","version":"0.0.1","event":"create_poll","data":{"poll_id":21}}"#; - let expected2 = r#"EVENT_JSON:{"standard":"ndc-easy-polls","version":"0.0.1","event":"respond","data":{"poll_id":22}}"#; + let expected1 = r#"EVENT_JSON:{"standard":"ndc-easy-poll","version":"1.0.0","event":"create_poll","data":{"poll_id":21}}"#; + let expected2 = r#"EVENT_JSON:{"standard":"ndc-easy-poll","version":"1.0.0","event":"respond","data":{"poll_id":22,"responder":"user-1.near"}}"#; emit_create_poll(21); assert_eq!(vec![expected1], test_utils::get_logs()); - emit_respond(22); + emit_respond(22, acc(1)); assert_eq!(vec![expected1, expected2], test_utils::get_logs()); } } diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 3412c507..a7243609 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -17,6 +17,7 @@ mod storage; pub const RESPOND_COST: Balance = MILI_NEAR; pub const RESPOND_CALLBACK_GAS: Gas = Gas(2 * Gas::ONE_TERA.0); +pub const MAX_TEXT_ANSWER_LEN: usize = 500; // TODO: decide on the maximum length of the text answers to #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] @@ -26,7 +27,7 @@ pub struct Contract { /// map of all results summarized pub results: LookupMap, /// map of all answers, (poll, user) -> vec of answers - pub answers: UnorderedMap<(PollId, AccountId), Vec>>, + pub answers: LookupMap<(PollId, AccountId), Vec>>, /// text answers are stored in a separate map. Key is a (pollId, question index). pub text_answers: LookupMap<(PollId, usize), Vector>, /// SBT registry. @@ -42,7 +43,7 @@ impl Contract { Self { polls: UnorderedMap::new(StorageKey::Polls), results: LookupMap::new(StorageKey::Results), - answers: UnorderedMap::new(StorageKey::Answers), + answers: LookupMap::new(StorageKey::Answers), text_answers: LookupMap::new(StorageKey::TextAnswers), registry, next_poll_id: 1, @@ -75,15 +76,15 @@ impl Contract { poll_id: u64, question: usize, from_answer: usize, - ) -> TextResponse<(bool, Vec)> { - // We cannot return more than 20 due to gas limit per txn. - self._result_text_answers(poll_id, question, from_answer, 20) + ) -> TextResponse<(Vec, bool)> { + // We cannot return more than 100 due to gas limit per txn. + self._text_answers(poll_id, question, from_answer, 100) } /// Returns a fixed value of answers // Function must be called until true is returned -> meaning all the answers were returned // `question` must be an index of the text question in the poll - pub fn _result_text_answers( + pub fn _text_answers( &self, poll_id: u64, question: usize, @@ -161,11 +162,11 @@ impl Contract { poll_id } - /// user can change his answer when the poll is still active - // TODO: currently we do not allow users to change the answer + /// Allows user to respond to a poll, once the answers are submited they cannot be changed. /// it panics if /// - poll not found /// - poll not active + /// - user alredy answered /// - poll.verified_humans_only is true, and user is not verified on IAH /// - user tries to vote with an invalid answer to a question /// emits repond event @@ -184,8 +185,6 @@ impl Contract { self.assert_active(poll_id)?; - // TODO: I think we should add a option for the poll creator to choose whether changing - // the answers while the poll is active is allowed or not self.assert_answered(poll_id, &caller)?; let poll = match self.polls.get(&poll_id) { None => return Err(PollError::NotFound), @@ -201,9 +200,7 @@ impl Contract { .on_human_verifed(true, caller, poll_id, answers), ); } else { - Self::ext(env::current_account_id()) - .with_static_gas(RESPOND_CALLBACK_GAS) - .on_human_verifed(false, caller, poll_id, answers); + self.on_human_verifed(vec![], false, caller, poll_id, answers)? } Ok(()) } @@ -273,14 +270,19 @@ impl Contract { num: results.num + 1 as u64, }); } - (Some(Answer::TextAnswer(answer)), _) => { + (Some(Answer::TextAnswer(answer)), PollResult::TextAnswer) => { let mut answers = self .text_answers .get(&(poll_id, i)) .expect(&format!("question not found for index {:?}", i)); + + if answer.len() > MAX_TEXT_ANSWER_LEN { + return Err(PollError::AnswerTooLong(answer.len())); + } answers.push(answer); self.text_answers.insert(&(poll_id, i), &answers); } + // if the answer is not provided for a question None is pushed as an anser to keep the integrity (None, _) => { unwrapped_answers.push(None); } @@ -295,12 +297,10 @@ impl Contract { .get(&(poll_id, caller.clone())) .unwrap_or(Vec::new()); answers.append(&mut unwrapped_answers); - self.answers.insert(&(poll_id, caller), &answers); - // update the status and number of participants - poll_results.status = Status::Active; + self.answers.insert(&(poll_id, caller.clone()), &answers); poll_results.participants += 1; self.results.insert(&poll_id, &poll_results); - emit_respond(poll_id); + emit_respond(poll_id, caller); Ok(()) } @@ -458,21 +458,28 @@ mod tests { } fn mk_batch_text_answers( + ctx: &mut VMContext, ctr: &mut Contract, predecessor: AccountId, poll_id: PollId, num_answers: u64, ) { - for i in 0..num_answers { + for _ in 0..num_answers { + testing_env!(ctx.clone()); let res = ctr.on_human_verifed( vec![], false, predecessor.clone(), poll_id, - vec![Some(Answer::TextAnswer(format!( - "Answer Answer Answer Answer Answer Answer Answer Answer Answer{}", - i - )))], + vec![Some(Answer::TextAnswer( + "wRjLbQZKutS0PCDx7F9pm5HgdO2h6vYcnlzBq3sEkU1f84aMyViAXTNjIoWPeLrVGvMm8 + HQZ7ij4J9gKdmMIsN5FB2wXfYuEkRlLTbn3DpGePo1VSqaAhYcC6W0Ou8ztvrxXnaxVbX1 + lMoXJ1YKvIksRnmQHD0VdW9GZrATg28pzUhqyfcBCjaoR6xs45Lu73Fw1PtevOYINaan3 + wRjLbQZKutS0PCDx7F9pm5HgdO2h6vYcnlzBq3sEkU1f84aMyViAXTNjIoWPeLrVGvMm8 + HQZ7ij4J9gKdmMIsN5FB2wXfYuEkRlLTbn3DpGePo1VSqaAhYcC6W0Ou8ztvrxXnaxVbX1 + lMoXJ1YKvIksRnmQHD0VdW9GZrATg28pzUhqyfcBCjao" + .to_owned(), + ))], ); assert!(res.is_ok()); } @@ -520,7 +527,7 @@ mod tests { String::from(""), String::from(""), ); - let expected_event = r#"EVENT_JSON:{"standard":"ndc-easy-polls","version":"0.0.1","event":"create_poll","data":{"poll_id":1}}"#; + let expected_event = r#"EVENT_JSON:{"standard":"ndc-easy-poll","version":"1.0.0","event":"create_poll","data":{"poll_id":1}}"#; assert!(test_utils::get_logs().len() == 1); assert_eq!(test_utils::get_logs()[0], expected_event); } @@ -599,7 +606,7 @@ mod tests { #[test] fn result_text_answers_poll_not_found() { let (_, ctr) = setup(&alice()); - match ctr.result_text_answers(0, 0, 0) { + match ctr.text_answers(0, 0, 0) { TextResponse::PollNotFound => (), other => panic!("Expected TextResponse::PollNotFound, but got {:?}", other), }; @@ -618,7 +625,7 @@ mod tests { String::from(""), String::from(""), ); - match ctr.result_text_answers(poll_id, 1, 0) { + match ctr.text_answers(poll_id, 1, 0) { TextResponse::QuestionNotFound => (), other => panic!( "Expected TextResponse::QuestionNotFound, but got {:?}", @@ -640,7 +647,7 @@ mod tests { String::from(""), String::from(""), ); - match ctr.result_text_answers(poll_id, 0, 0) { + match ctr.text_answers(poll_id, 0, 0) { TextResponse::QuestionWrongType => (), other => panic!( "Expected TextResponse::QuestionWrongType, but got {:?}", @@ -724,7 +731,7 @@ mod tests { ); assert!(res.is_ok()); - let expected_event = r#"EVENT_JSON:{"standard":"ndc-easy-polls","version":"0.0.1","event":"respond","data":{"poll_id":1}}"#; + let expected_event = r#"EVENT_JSON:{"standard":"ndc-easy-poll","version":"1.0.0","event":"respond","data":{"poll_id":1,"responder":"alice.near"}}"#; assert!(test_utils::get_logs().len() == 1); assert_eq!(test_utils::get_logs()[0], expected_event); @@ -740,7 +747,6 @@ mod tests { assert!(res.is_ok()); assert!(test_utils::get_logs().len() == 1); - assert_eq!(test_utils::get_logs()[0], expected_event); ctx.predecessor_account_id = charlie(); testing_env!(ctx.clone()); @@ -754,7 +760,6 @@ mod tests { assert!(res.is_ok()); assert!(test_utils::get_logs().len() == 1); - assert_eq!(test_utils::get_logs()[0], expected_event); let results = ctr.results(poll_id); assert_eq!( @@ -1006,16 +1011,16 @@ mod tests { results: vec![PollResult::TextAnswer] } ); - let text_answers = ctr.result_text_answers(poll_id, 0, 0); + let text_answers = ctr.text_answers(poll_id, 0, 0); assert_eq!( text_answers, - TextResponse::Ok((true, vec![answer1, answer2, answer3])) + TextResponse::Ok((vec![answer1, answer2, answer3], true)) ); } #[test] fn result_text_answers() { - let (_, mut ctr) = setup(&alice()); + let (mut ctx, mut ctr) = setup(&alice()); let poll_id = ctr.create_poll( false, vec![question_text_answers(true)], @@ -1026,11 +1031,19 @@ mod tests { String::from(""), String::from(""), ); - mk_batch_text_answers(&mut ctr, alice(), poll_id, 50); + mk_batch_text_answers(&mut ctx, &mut ctr, alice(), poll_id, 200); // depending on the lenght of the answers the limit decreases rappidly - let text_answers = ctr._result_text_answers(poll_id, 0, 0, 30); + let text_answers = ctr._text_answers(poll_id, 0, 0, 100); + match text_answers { + TextResponse::Ok((_, false)) => {} + _ => panic!( + "Expected TextResponse::Ok with false, but got {:?}", + text_answers + ), + } + let text_answers = ctr._text_answers(poll_id, 0, 101, 100); match text_answers { - TextResponse::Ok((false, _)) => {} + TextResponse::Ok((_, true)) => {} _ => panic!( "Expected TextResponse::Ok with true, but got {:?}", text_answers From b9b4a56b08fd893203359d04aa8f7edcade82b53 Mon Sep 17 00:00:00 2001 From: sczembor Date: Mon, 11 Sep 2023 21:01:32 +0200 Subject: [PATCH 32/42] test fix --- contracts/easy-poll/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index a7243609..4f927306 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -765,7 +765,7 @@ mod tests { assert_eq!( results.unwrap(), Results { - status: Status::Active, + status: Status::NotStarted, participants: 3, results: vec![PollResult::YesNo((2, 1)),] } @@ -891,7 +891,7 @@ mod tests { assert_eq!( results.unwrap(), Results { - status: Status::Active, + status: Status::NotStarted, participants: 3, results: vec![PollResult::OpinionRange(OpinionRangeResult { sum: 17, @@ -948,7 +948,7 @@ mod tests { assert_eq!( results.unwrap(), Results { - status: Status::Active, + status: Status::NotStarted, participants: 3, results: vec![PollResult::TextChoices(vec![2, 1, 0]),] } @@ -1006,7 +1006,7 @@ mod tests { assert_eq!( results.unwrap(), Results { - status: Status::Active, + status: Status::NotStarted, participants: 3, results: vec![PollResult::TextAnswer] } From ab96d0be86c9ac922e3e22ba418a15909034fd98 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:30:35 +0200 Subject: [PATCH 33/42] Apply suggestions from code review Co-authored-by: Robert Zaremba --- contracts/easy-poll/src/errors.rs | 6 +++--- contracts/easy-poll/src/lib.rs | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs index 9f49eafd..3e3ddb2e 100644 --- a/contracts/easy-poll/src/errors.rs +++ b/contracts/easy-poll/src/errors.rs @@ -8,7 +8,7 @@ use crate::MAX_TEXT_ANSWER_LEN; #[derive(Debug)] pub enum PollError { RequiredAnswer(usize), - NoSBTs, + NotIAH, NotFound, NotActive, OpinionRange, @@ -24,10 +24,10 @@ impl FunctionError for PollError { PollError::RequiredAnswer(index) => { panic_str(&format!("Answer to a required question index={} was not provided",index)) } - PollError::NoSBTs => panic_str("voter is not a verified human"), + PollError::NotIAH => panic_str("voter is not a verified human"), PollError::NotFound => panic_str("poll not found"), PollError::NotActive => panic_str("poll is not active"), - PollError::OpinionRange => panic_str("opinion must be between 0 and 10"), + PollError::OpinionRange => panic_str("opinion must be between 1 and 10"), PollError::WrongAnswer => { panic_str("answer provied does not match the expected question") }, diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 4f927306..96a55d87 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -262,7 +262,7 @@ impl Contract { poll_results.results[i] = PollResult::PictureChoices(res); } (Some(Answer::OpinionRange(answer)), PollResult::OpinionRange(results)) => { - if *answer > 10 { + if *answer < 1 || *answer > 10 { return Err(PollError::OpinionRange); } poll_results.results[i] = PollResult::OpinionRange(OpinionRangeResult { @@ -289,6 +289,7 @@ impl Contract { (_, _) => return Err(PollError::WrongAnswer), } if answers[i].is_some() { + // None case is handled in the `match` statement. unwrapped_answers.push(Some(answers[i].clone().unwrap())); } } From ab8cc91c0b8a08680214e05694ed1eaca0f3d5d2 Mon Sep 17 00:00:00 2001 From: sczembor Date: Tue, 12 Sep 2023 15:35:01 +0200 Subject: [PATCH 34/42] fix --- contracts/easy-poll/src/lib.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 96a55d87..78c7a9dc 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -221,7 +221,7 @@ impl Contract { answers: Vec>, ) -> Result<(), PollError> { if iah_only && tokens.is_empty() { - return Err(PollError::NoSBTs); + return Err(PollError::NotIAH); } let questions: Vec = self.polls.get(&poll_id).expect("poll not found").questions; if questions.len() != answers.len() { @@ -1077,8 +1077,8 @@ mod tests { Err(err) => { println!("Received error: {:?}", err); match err { - PollError::NoSBTs => { - println!("Expected error: PollError::NoSBTs") + PollError::NotIAH => { + println!("Expected error: PollError::NotIAH") } _ => panic!("Unexpected error: {:?}", err), } From 249fedd1fc02337571e21fa1bd798eb4d1ccbde8 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Tue, 12 Sep 2023 15:37:53 +0200 Subject: [PATCH 35/42] Update contracts/easy-poll/src/lib.rs Co-authored-by: Robert Zaremba --- contracts/easy-poll/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 78c7a9dc..9430c670 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -231,7 +231,9 @@ impl Contract { let mut poll_results = self.results.get(&poll_id).expect("results not found"); for i in 0..questions.len() { - if questions[i].required && answers[i].is_none() { + let mut q = &questions[i]; + let a = &answers[i]; + if q.required && a.is_none() { return Err(PollError::RequiredAnswer(i)); } From 397752a5d035f93c18f1f8925e9e02cb2b9cdd46 Mon Sep 17 00:00:00 2001 From: sczembor Date: Tue, 12 Sep 2023 15:38:13 +0200 Subject: [PATCH 36/42] move debug to test only --- contracts/easy-poll/src/errors.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/contracts/easy-poll/src/errors.rs b/contracts/easy-poll/src/errors.rs index 3e3ddb2e..a6efc96b 100644 --- a/contracts/easy-poll/src/errors.rs +++ b/contracts/easy-poll/src/errors.rs @@ -4,8 +4,7 @@ use near_sdk::FunctionError; use crate::MAX_TEXT_ANSWER_LEN; /// Contract errors -#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq))] -#[derive(Debug)] +#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] pub enum PollError { RequiredAnswer(usize), NotIAH, From 3a4c741bf928e5a7e0af2954b6d5002dc32c076a Mon Sep 17 00:00:00 2001 From: sczembor Date: Tue, 12 Sep 2023 16:42:07 +0200 Subject: [PATCH 37/42] refactor on_human_verifed and initalize_results methods --- contracts/easy-poll/src/lib.rs | 118 ++++++++++++++++----------------- 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 9430c670..c6a1a27c 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -143,7 +143,7 @@ impl Contract { ); let poll_id = self.next_poll_id; self.next_poll_id += 1; - self.initalize_results(poll_id, &questions); + self.initialize_results(poll_id, &questions); self.polls.insert( &poll_id, &Poll { @@ -220,57 +220,52 @@ impl Contract { poll_id: PollId, answers: Vec>, ) -> Result<(), PollError> { + // Check for IAH requirement if iah_only is set if iah_only && tokens.is_empty() { return Err(PollError::NotIAH); } + + // Retrieve questions and poll results let questions: Vec = self.polls.get(&poll_id).expect("poll not found").questions; + let mut poll_results = self.results.get(&poll_id).expect("results not found"); + + // Check if the number of answers matches the number of questions if questions.len() != answers.len() { return Err(PollError::IncorrectAnswerVector); } + + // Initialize unwrapped_answers vector let mut unwrapped_answers: Vec> = Vec::new(); - let mut poll_results = self.results.get(&poll_id).expect("results not found"); for i in 0..questions.len() { - let mut q = &questions[i]; + let q = &questions[i]; let a = &answers[i]; if q.required && a.is_none() { return Err(PollError::RequiredAnswer(i)); } - match (&answers[i], &poll_results.results[i]) { - (Some(Answer::YesNo(answer)), PollResult::YesNo((yes, no))) => { - if *answer { - poll_results.results[i] = PollResult::YesNo((*yes + 1, *no)); + match (a, &mut poll_results.results[i]) { + (Some(Answer::YesNo(response)), PollResult::YesNo((yes_count, no_count))) => { + if *response { + *yes_count += 1; } else { - poll_results.results[i] = PollResult::YesNo((*yes, *no + 1)); + *no_count += 1; } } - (Some(Answer::TextChoices(answer)), PollResult::TextChoices(results)) => { - let mut res: Vec = results.to_vec(); - for i in 0..answer.len() { - if answer[i] == true { - res[i] += 1; + (Some(Answer::TextChoices(choices)), PollResult::TextChoices(results)) + | (Some(Answer::PictureChoices(choices)), PollResult::PictureChoices(results)) => { + for (j, choice) in choices.iter().enumerate() { + if *choice { + results[j] += 1; } } - poll_results.results[i] = PollResult::TextChoices(res); } - (Some(Answer::PictureChoices(answer)), PollResult::PictureChoices(results)) => { - let mut res: Vec = results.to_vec(); - for i in 0..answer.len() { - if answer[i] == true { - res[i] += 1; - } - } - poll_results.results[i] = PollResult::PictureChoices(res); - } - (Some(Answer::OpinionRange(answer)), PollResult::OpinionRange(results)) => { - if *answer < 1 || *answer > 10 { + (Some(Answer::OpinionRange(opinion)), PollResult::OpinionRange(results)) => { + if *opinion < 1 || *opinion > 10 { return Err(PollError::OpinionRange); } - poll_results.results[i] = PollResult::OpinionRange(OpinionRangeResult { - sum: results.sum + *answer as u64, - num: results.num + 1 as u64, - }); + results.sum += *opinion as u64; + results.num += 1; } (Some(Answer::TextAnswer(answer)), PollResult::TextAnswer) => { let mut answers = self @@ -295,15 +290,22 @@ impl Contract { unwrapped_answers.push(Some(answers[i].clone().unwrap())); } } - let mut answers = self + // Update answers for the caller + let mut caller_answers = self .answers .get(&(poll_id, caller.clone())) .unwrap_or(Vec::new()); - answers.append(&mut unwrapped_answers); - self.answers.insert(&(poll_id, caller.clone()), &answers); + caller_answers.append(&mut unwrapped_answers); + self.answers + .insert(&(poll_id, caller.clone()), &caller_answers); + + // Update participants count and poll results poll_results.participants += 1; + + // Update results and emit response event self.results.insert(&poll_id, &poll_results); emit_respond(poll_id, caller); + Ok(()) } @@ -330,41 +332,35 @@ impl Contract { Ok(()) } - fn initalize_results(&mut self, poll_id: PollId, questions: &Vec) { - let mut results = Vec::new(); + fn initialize_results(&mut self, poll_id: PollId, questions: &[Question]) { let mut index = 0; - for question in questions { - match question.question_type { - Answer::YesNo(_) => results.push(PollResult::YesNo((0, 0))), - Answer::TextChoices(_) => results.push(PollResult::TextChoices(vec![ - 0; - question - .choices - .clone() - .unwrap() - .len() - ])), - Answer::PictureChoices(_) => results.push(PollResult::PictureChoices(Vec::new())), - Answer::OpinionRange(_) => { - results.push(PollResult::OpinionRange(OpinionRangeResult { - sum: 0, - num: 0, - })) - } - Answer::TextAnswer(_) => { - results.push(PollResult::TextAnswer); - self.text_answers - .insert(&(poll_id, index), &Vector::new(StorageKey::TextAnswers)); - } - }; - index += 1; - } + let results: Vec = questions + .iter() + .map(|question| { + let result = match &question.question_type { + Answer::YesNo(_) => PollResult::YesNo((0, 0)), + Answer::TextChoices(choices) => PollResult::TextChoices(vec![0; choices.len()]), + Answer::PictureChoices(_) => PollResult::PictureChoices(Vec::new()), + Answer::OpinionRange(_) => { + PollResult::OpinionRange(OpinionRangeResult { sum: 0, num: 0 }) + } + Answer::TextAnswer(_) => { + self.text_answers + .insert(&(poll_id, index), &Vector::new(StorageKey::TextAnswers)); + PollResult::TextAnswer + } + }; + index += 1; + result + }) + .collect(); + self.results.insert( &poll_id, &Results { status: Status::NotStarted, participants: 0, - results: results, + results, }, ); } From f5a35106a67ce16c11e6e99fd3efd94e1943290e Mon Sep 17 00:00:00 2001 From: sczembor Date: Wed, 13 Sep 2023 12:31:22 +0200 Subject: [PATCH 38/42] do not store answers on chain --- contracts/easy-poll/src/lib.rs | 290 +++-------------------------- contracts/easy-poll/src/storage.rs | 15 +- 2 files changed, 25 insertions(+), 280 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index c6a1a27c..72005d8b 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -6,7 +6,8 @@ pub use crate::storage::*; use cost::MILI_NEAR; use ext::ext_registry; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; -use near_sdk::collections::{LookupMap, UnorderedMap, Vector}; +use near_sdk::collections::LookupSet; +use near_sdk::collections::{LookupMap, UnorderedMap}; use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; use near_sdk::{Balance, Gas}; @@ -26,10 +27,8 @@ pub struct Contract { pub polls: UnorderedMap, /// map of all results summarized pub results: LookupMap, - /// map of all answers, (poll, user) -> vec of answers - pub answers: LookupMap<(PollId, AccountId), Vec>>, - /// text answers are stored in a separate map. Key is a (pollId, question index). - pub text_answers: LookupMap<(PollId, usize), Vector>, + /// lookup set of (poll_id, responder) + pub participants: LookupSet<(PollId, AccountId)>, /// SBT registry. pub registry: AccountId, /// next poll id @@ -43,8 +42,7 @@ impl Contract { Self { polls: UnorderedMap::new(StorageKey::Polls), results: LookupMap::new(StorageKey::Results), - answers: LookupMap::new(StorageKey::Answers), - text_answers: LookupMap::new(StorageKey::TextAnswers), + participants: LookupSet::new(StorageKey::Participants), registry, next_poll_id: 1, } @@ -59,63 +57,11 @@ impl Contract { self.polls.get(&poll_id) } - /// Returns caller response to the specified poll. It doesn't return text responses of the given poll ID. - pub fn my_response(&self, poll_id: PollId) -> Option>> { - let caller = env::predecessor_account_id(); - self.answers.get(&(poll_id, caller)) - } - /// Returns poll results (except for text answers), if poll not found returns None. pub fn results(&self, poll_id: u64) -> Option { self.results.get(&poll_id) } - /// Returns text answers in rounds. Starts from the question id provided. Needs to be called until true is returned. - pub fn text_answers( - &self, - poll_id: u64, - question: usize, - from_answer: usize, - ) -> TextResponse<(Vec, bool)> { - // We cannot return more than 100 due to gas limit per txn. - self._text_answers(poll_id, question, from_answer, 100) - } - - /// Returns a fixed value of answers - // Function must be called until true is returned -> meaning all the answers were returned - // `question` must be an index of the text question in the poll - pub fn _text_answers( - &self, - poll_id: u64, - question: usize, - from_answer: usize, - limit: usize, - ) -> TextResponse<(Vec, bool)> { - let poll = match self.polls.get(&poll_id) { - Some(poll) => poll, - None => return TextResponse::PollNotFound, - }; - - match poll.questions.get(question) { - Some(questions) => questions, - None => return TextResponse::QuestionNotFound, - }; - - let text_answers = match self.text_answers.get(&(poll_id, question)) { - Some(text_answers) => text_answers, - None => return TextResponse::QuestionWrongType, - }; - let to_return; - let mut finished = false; - if from_answer + limit > text_answers.len() as usize { - to_return = text_answers.to_vec()[from_answer..].to_vec(); - finished = true; - } else { - to_return = text_answers.to_vec()[from_answer..from_answer + limit].to_vec(); - } - TextResponse::Ok((to_return, finished)) - } - /********** * TRANSACTIONS **********/ @@ -137,10 +83,7 @@ impl Contract { link: String, ) -> PollId { let created_at = env::block_timestamp_ms(); - require!( - created_at < starts_at, - "poll start must be in the future".to_string() - ); + require!(created_at < starts_at, "poll start must be in the future"); let poll_id = self.next_poll_id; self.next_poll_id += 1; self.initialize_results(poll_id, &questions); @@ -234,9 +177,6 @@ impl Contract { return Err(PollError::IncorrectAnswerVector); } - // Initialize unwrapped_answers vector - let mut unwrapped_answers: Vec> = Vec::new(); - for i in 0..questions.len() { let q = &questions[i]; let a = &answers[i]; @@ -268,39 +208,21 @@ impl Contract { results.num += 1; } (Some(Answer::TextAnswer(answer)), PollResult::TextAnswer) => { - let mut answers = self - .text_answers - .get(&(poll_id, i)) - .expect(&format!("question not found for index {:?}", i)); - if answer.len() > MAX_TEXT_ANSWER_LEN { return Err(PollError::AnswerTooLong(answer.len())); } - answers.push(answer); - self.text_answers.insert(&(poll_id, i), &answers); - } - // if the answer is not provided for a question None is pushed as an anser to keep the integrity - (None, _) => { - unwrapped_answers.push(None); } + // if the answer is not provided do nothing + (None, _) => {} (_, _) => return Err(PollError::WrongAnswer), } - if answers[i].is_some() { - // None case is handled in the `match` statement. - unwrapped_answers.push(Some(answers[i].clone().unwrap())); - } } - // Update answers for the caller - let mut caller_answers = self - .answers - .get(&(poll_id, caller.clone())) - .unwrap_or(Vec::new()); - caller_answers.append(&mut unwrapped_answers); - self.answers - .insert(&(poll_id, caller.clone()), &caller_answers); - // Update participants count and poll results - poll_results.participants += 1; + // Update participants count + poll_results.participants_num += 1; + + // Update the participants lookupset to ensure user cannot answer twice + self.participants.insert(&(poll_id, caller.clone())); // Update results and emit response event self.results.insert(&poll_id, &poll_results); @@ -326,7 +248,7 @@ impl Contract { } fn assert_answered(&self, poll_id: PollId, caller: &AccountId) -> Result<(), PollError> { - if self.answers.get(&(poll_id, caller.clone())).is_some() { + if self.participants.contains(&(poll_id, caller.clone())) { return Err(PollError::AlredyAnswered); } Ok(()) @@ -344,11 +266,7 @@ impl Contract { Answer::OpinionRange(_) => { PollResult::OpinionRange(OpinionRangeResult { sum: 0, num: 0 }) } - Answer::TextAnswer(_) => { - self.text_answers - .insert(&(poll_id, index), &Vector::new(StorageKey::TextAnswers)); - PollResult::TextAnswer - } + Answer::TextAnswer(_) => PollResult::TextAnswer, }; index += 1; result @@ -359,7 +277,7 @@ impl Contract { &poll_id, &Results { status: Status::NotStarted, - participants: 0, + participants_num: 0, results, }, ); @@ -374,8 +292,8 @@ mod tests { }; use crate::{ - Answer, Contract, OpinionRangeResult, PollError, PollId, PollResult, Question, Results, - Status, TextResponse, RESPOND_COST, + Answer, Contract, OpinionRangeResult, PollError, PollResult, Question, Results, Status, + RESPOND_COST, }; const MILI_SECOND: u64 = 1000000; // nanoseconds @@ -456,34 +374,6 @@ mod tests { } } - fn mk_batch_text_answers( - ctx: &mut VMContext, - ctr: &mut Contract, - predecessor: AccountId, - poll_id: PollId, - num_answers: u64, - ) { - for _ in 0..num_answers { - testing_env!(ctx.clone()); - let res = ctr.on_human_verifed( - vec![], - false, - predecessor.clone(), - poll_id, - vec![Some(Answer::TextAnswer( - "wRjLbQZKutS0PCDx7F9pm5HgdO2h6vYcnlzBq3sEkU1f84aMyViAXTNjIoWPeLrVGvMm8 - HQZ7ij4J9gKdmMIsN5FB2wXfYuEkRlLTbn3DpGePo1VSqaAhYcC6W0Ou8ztvrxXnaxVbX1 - lMoXJ1YKvIksRnmQHD0VdW9GZrATg28pzUhqyfcBCjaoR6xs45Lu73Fw1PtevOYINaan3 - wRjLbQZKutS0PCDx7F9pm5HgdO2h6vYcnlzBq3sEkU1f84aMyViAXTNjIoWPeLrVGvMm8 - HQZ7ij4J9gKdmMIsN5FB2wXfYuEkRlLTbn3DpGePo1VSqaAhYcC6W0Ou8ztvrxXnaxVbX1 - lMoXJ1YKvIksRnmQHD0VdW9GZrATg28pzUhqyfcBCjao" - .to_owned(), - ))], - ); - assert!(res.is_ok()); - } - } - fn setup(predecessor: &AccountId) -> (VMContext, Contract) { let mut ctx = VMContextBuilder::new() .predecessor_account_id(alice()) @@ -531,49 +421,6 @@ mod tests { assert_eq!(test_utils::get_logs()[0], expected_event); } - #[test] - fn my_response_not_found() { - let (_, mut ctr) = setup(&alice()); - let poll_id = ctr.create_poll( - false, - vec![question_yes_no(true)], - 2, - 100, - String::from("Hello, world!"), - tags(), - String::from(""), - String::from(""), - ); - assert!(ctr.my_response(poll_id).is_none()); - } - - #[test] - fn my_response() { - let (mut ctx, mut ctr) = setup(&alice()); - let poll_id = ctr.create_poll( - false, - vec![question_yes_no(false), question_yes_no(true)], - 2, - 100, - String::from("Hello, world!"), - tags(), - String::from(""), - String::from(""), - ); - ctx.block_timestamp = MILI_SECOND * 3; - testing_env!(ctx.clone()); - let res = ctr.on_human_verifed( - vec![], - false, - ctx.predecessor_account_id, - poll_id, - vec![None, Some(Answer::YesNo(true))], - ); - assert!(res.is_ok()); - let res = ctr.my_response(poll_id); - assert_eq!(res.unwrap(), vec![None, Some(Answer::YesNo(true))]) - } - #[test] fn results_poll_not_found() { let (_, ctr) = setup(&alice()); @@ -596,65 +443,12 @@ mod tests { let res = ctr.results(poll_id); let expected = Results { status: Status::NotStarted, - participants: 0, + participants_num: 0, results: vec![PollResult::YesNo((0, 0))], }; assert_eq!(res.unwrap(), expected); } - #[test] - fn result_text_answers_poll_not_found() { - let (_, ctr) = setup(&alice()); - match ctr.text_answers(0, 0, 0) { - TextResponse::PollNotFound => (), - other => panic!("Expected TextResponse::PollNotFound, but got {:?}", other), - }; - } - - #[test] - fn result_text_answers_question_not_found() { - let (_, mut ctr) = setup(&alice()); - let poll_id = ctr.create_poll( - false, - vec![question_yes_no(true)], - 2, - 100, - String::from("Hello, world!"), - tags(), - String::from(""), - String::from(""), - ); - match ctr.text_answers(poll_id, 1, 0) { - TextResponse::QuestionNotFound => (), - other => panic!( - "Expected TextResponse::QuestionNotFound, but got {:?}", - other - ), - }; - } - - #[test] - fn result_text_answers_question_wrong_type() { - let (_, mut ctr) = setup(&alice()); - let poll_id = ctr.create_poll( - false, - vec![question_yes_no(true)], - 2, - 100, - String::from("Hello, world!"), - tags(), - String::from(""), - String::from(""), - ); - match ctr.text_answers(poll_id, 0, 0) { - TextResponse::QuestionWrongType => (), - other => panic!( - "Expected TextResponse::QuestionWrongType, but got {:?}", - other - ), - }; - } - #[test] #[should_panic(expected = "attached_deposit not sufficient")] fn respond_wrong_deposit() { @@ -765,7 +559,7 @@ mod tests { results.unwrap(), Results { status: Status::NotStarted, - participants: 3, + participants_num: 3, results: vec![PollResult::YesNo((2, 1)),] } ) @@ -891,7 +685,7 @@ mod tests { results.unwrap(), Results { status: Status::NotStarted, - participants: 3, + participants_num: 3, results: vec![PollResult::OpinionRange(OpinionRangeResult { sum: 17, num: 3 @@ -948,7 +742,7 @@ mod tests { results.unwrap(), Results { status: Status::NotStarted, - participants: 3, + participants_num: 3, results: vec![PollResult::TextChoices(vec![2, 1, 0]),] } ) @@ -1006,48 +800,10 @@ mod tests { results.unwrap(), Results { status: Status::NotStarted, - participants: 3, + participants_num: 3, results: vec![PollResult::TextAnswer] } ); - let text_answers = ctr.text_answers(poll_id, 0, 0); - assert_eq!( - text_answers, - TextResponse::Ok((vec![answer1, answer2, answer3], true)) - ); - } - - #[test] - fn result_text_answers() { - let (mut ctx, mut ctr) = setup(&alice()); - let poll_id = ctr.create_poll( - false, - vec![question_text_answers(true)], - 2, - 100, - String::from("Hello, world!"), - tags(), - String::from(""), - String::from(""), - ); - mk_batch_text_answers(&mut ctx, &mut ctr, alice(), poll_id, 200); - // depending on the lenght of the answers the limit decreases rappidly - let text_answers = ctr._text_answers(poll_id, 0, 0, 100); - match text_answers { - TextResponse::Ok((_, false)) => {} - _ => panic!( - "Expected TextResponse::Ok with false, but got {:?}", - text_answers - ), - } - let text_answers = ctr._text_answers(poll_id, 0, 101, 100); - match text_answers { - TextResponse::Ok((_, true)) => {} - _ => panic!( - "Expected TextResponse::Ok with true, but got {:?}", - text_answers - ), - } } #[test] diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 02832b9d..04f01085 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -68,7 +68,7 @@ pub struct Poll { #[serde(crate = "near_sdk::serde")] pub struct Results { pub status: Status, - pub participants: u64, // number of participants + pub participants_num: u64, // number of participants pub results: Vec, // question_id, result (sum of yes etc.) } @@ -85,16 +85,5 @@ pub enum Status { pub enum StorageKey { Polls, Results, - Answers, - TextAnswers, -} - -#[derive(Deserialize, Serialize)] -#[cfg_attr(not(target_arch = "wasm32"), derive(PartialEq, Debug))] -#[serde(crate = "near_sdk::serde")] -pub enum TextResponse { - Ok(T), - PollNotFound, - QuestionNotFound, - QuestionWrongType, + Participants, } From ef83ec9a0957fcdc95f57f1e336f4119d00f0cab Mon Sep 17 00:00:00 2001 From: sczembor Date: Wed, 13 Sep 2023 12:53:17 +0200 Subject: [PATCH 39/42] apply code review suggestions --- contracts/easy-poll/src/lib.rs | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 72005d8b..f598f36d 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -6,8 +6,8 @@ pub use crate::storage::*; use cost::MILI_NEAR; use ext::ext_registry; use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize}; +use near_sdk::collections::LookupMap; use near_sdk::collections::LookupSet; -use near_sdk::collections::{LookupMap, UnorderedMap}; use near_sdk::{env, near_bindgen, require, AccountId, PanicOnDefault}; use near_sdk::{Balance, Gas}; @@ -24,7 +24,7 @@ pub const MAX_TEXT_ANSWER_LEN: usize = 500; // TODO: decide on the maximum lengt #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] pub struct Contract { /// map of all polls - pub polls: UnorderedMap, + pub polls: LookupMap, /// map of all results summarized pub results: LookupMap, /// lookup set of (poll_id, responder) @@ -40,7 +40,7 @@ impl Contract { #[init] pub fn new(registry: AccountId) -> Self { Self { - polls: UnorderedMap::new(StorageKey::Polls), + polls: LookupMap::new(StorageKey::Polls), results: LookupMap::new(StorageKey::Results), participants: LookupSet::new(StorageKey::Participants), registry, @@ -128,7 +128,7 @@ impl Contract { self.assert_active(poll_id)?; - self.assert_answered(poll_id, &caller)?; + self.assert_not_answered(poll_id, &caller)?; let poll = match self.polls.get(&poll_id) { None => return Err(PollError::NotFound), Some(poll) => poll, @@ -169,8 +169,14 @@ impl Contract { } // Retrieve questions and poll results - let questions: Vec = self.polls.get(&poll_id).expect("poll not found").questions; - let mut poll_results = self.results.get(&poll_id).expect("results not found"); + let questions = match self.polls.get(&poll_id) { + Some(poll) => poll.questions, + None => return Err(PollError::NotFound), + }; + let mut poll_results = match self.results.get(&poll_id) { + Some(results) => results, + None => return Err(PollError::NotFound), + }; // Check if the number of answers matches the number of questions if questions.len() != answers.len() { @@ -180,9 +186,6 @@ impl Contract { for i in 0..questions.len() { let q = &questions[i]; let a = &answers[i]; - if q.required && a.is_none() { - return Err(PollError::RequiredAnswer(i)); - } match (a, &mut poll_results.results[i]) { (Some(Answer::YesNo(response)), PollResult::YesNo((yes_count, no_count))) => { @@ -213,7 +216,11 @@ impl Contract { } } // if the answer is not provided do nothing - (None, _) => {} + (None, _) => { + if q.required { + return Err(PollError::RequiredAnswer(i)); + } + } (_, _) => return Err(PollError::WrongAnswer), } } @@ -247,7 +254,7 @@ impl Contract { Ok(()) } - fn assert_answered(&self, poll_id: PollId, caller: &AccountId) -> Result<(), PollError> { + fn assert_not_answered(&self, poll_id: PollId, caller: &AccountId) -> Result<(), PollError> { if self.participants.contains(&(poll_id, caller.clone())) { return Err(PollError::AlredyAnswered); } From dd10a8e361ee277574f501d9a57fb25676845997 Mon Sep 17 00:00:00 2001 From: Robert Zaremba Date: Wed, 13 Sep 2023 13:30:11 +0200 Subject: [PATCH 40/42] Update contracts/easy-poll/src/lib.rs --- contracts/easy-poll/src/lib.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index f598f36d..191c4f82 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -225,13 +225,9 @@ impl Contract { } } - // Update participants count - poll_results.participants_num += 1; - // Update the participants lookupset to ensure user cannot answer twice self.participants.insert(&(poll_id, caller.clone())); - - // Update results and emit response event + poll_results.participants_num += 1; self.results.insert(&poll_id, &poll_results); emit_respond(poll_id, caller); From 046db48fa6b077600841c9a0d47326f530511a06 Mon Sep 17 00:00:00 2001 From: sczembor <43810037+sczembor@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:32:44 +0200 Subject: [PATCH 41/42] Update contracts/easy-poll/src/lib.rs Co-authored-by: Robert Zaremba --- contracts/easy-poll/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index 191c4f82..f83afe4b 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -30,7 +30,7 @@ pub struct Contract { /// lookup set of (poll_id, responder) pub participants: LookupSet<(PollId, AccountId)>, /// SBT registry. - pub registry: AccountId, + pub sbt_registry: AccountId, /// next poll id pub next_poll_id: PollId, } From 747d47933d73cd1d9661e7ed2704688f287b502d Mon Sep 17 00:00:00 2001 From: sczembor Date: Wed, 13 Sep 2023 13:48:56 +0200 Subject: [PATCH 42/42] change choices to unique vector rather than true/false --- contracts/easy-poll/src/lib.rs | 20 +++++++++----------- contracts/easy-poll/src/storage.rs | 6 +++--- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/contracts/easy-poll/src/lib.rs b/contracts/easy-poll/src/lib.rs index f83afe4b..f9c27d1a 100644 --- a/contracts/easy-poll/src/lib.rs +++ b/contracts/easy-poll/src/lib.rs @@ -38,12 +38,12 @@ pub struct Contract { #[near_bindgen] impl Contract { #[init] - pub fn new(registry: AccountId) -> Self { + pub fn new(sbt_registry: AccountId) -> Self { Self { polls: LookupMap::new(StorageKey::Polls), results: LookupMap::new(StorageKey::Results), participants: LookupSet::new(StorageKey::Participants), - registry, + sbt_registry, next_poll_id: 1, } } @@ -135,7 +135,7 @@ impl Contract { }; // if iah calls the registry to verify the iah sbt if poll.iah_only { - ext_registry::ext(self.registry.clone()) + ext_registry::ext(self.sbt_registry.clone()) .is_human(caller.clone()) .then( Self::ext(env::current_account_id()) @@ -197,10 +197,8 @@ impl Contract { } (Some(Answer::TextChoices(choices)), PollResult::TextChoices(results)) | (Some(Answer::PictureChoices(choices)), PollResult::PictureChoices(results)) => { - for (j, choice) in choices.iter().enumerate() { - if *choice { - results[j] += 1; - } + for choice in choices { + results[*choice as usize] += 1; } } (Some(Answer::OpinionRange(opinion)), PollResult::OpinionRange(results)) => { @@ -349,7 +347,7 @@ mod tests { fn question_text_choices(required: bool) -> Question { Question { - question_type: Answer::TextChoices(vec![false, false, false]), + question_type: Answer::TextChoices(vec![0, 1, 2]), required, title: String::from("Yes and no test!"), description: None, @@ -717,7 +715,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![Some(Answer::TextChoices(vec![true, false, false]))], + vec![Some(Answer::TextChoices(vec![0]))], ); assert!(res.is_ok()); ctx.predecessor_account_id = bob(); @@ -727,7 +725,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![Some(Answer::TextChoices(vec![true, false, false]))], + vec![Some(Answer::TextChoices(vec![0]))], ); assert!(res.is_ok()); ctx.predecessor_account_id = charlie(); @@ -737,7 +735,7 @@ mod tests { false, ctx.predecessor_account_id, poll_id, - vec![Some(Answer::TextChoices(vec![false, true, false]))], + vec![Some(Answer::TextChoices(vec![1]))], ); assert!(res.is_ok()); let results = ctr.results(poll_id); diff --git a/contracts/easy-poll/src/storage.rs b/contracts/easy-poll/src/storage.rs index 04f01085..1ac8891f 100644 --- a/contracts/easy-poll/src/storage.rs +++ b/contracts/easy-poll/src/storage.rs @@ -10,9 +10,9 @@ pub type PollId = u64; #[serde(crate = "near_sdk::serde")] pub enum Answer { YesNo(bool), - TextChoices(Vec), // should respect the min_choices, max_choices - PictureChoices(Vec), // should respect the min_choices, max_choices - OpinionRange(u8), // should be a number between 0 and 10 + TextChoices(Vec), // should respect the min_choices, max_choices + PictureChoices(Vec), // should respect the min_choices, max_choices + OpinionRange(u8), // should be a number between 0 and 10 TextAnswer(String), }