From 4eb2d8108eff551e9c8c827ff809255941398313 Mon Sep 17 00:00:00 2001 From: Mateusz Jasiuk Date: Wed, 29 May 2024 15:15:44 +0200 Subject: [PATCH] feat: tally type --- chain/src/main.rs | 31 ++++++++- chain/src/repository/gov.rs | 8 +-- chain/src/services/namada.rs | 64 ++++++++++++++++++- .../down.sql | 3 +- .../up.sql | 4 +- orm/src/governance_proposal.rs | 30 ++++++++- orm/src/schema.rs | 6 ++ shared/src/proposal.rs | 40 ++++++++++++ swagger.yml | 22 ++++--- webserver/src/response/governance.rs | 50 ++++++++++++--- webserver/src/service/balance.rs | 8 +-- webserver/src/service/mod.rs | 1 + webserver/src/service/pos.rs | 48 ++++++++++++-- webserver/src/service/utils.rs | 7 ++ 14 files changed, 281 insertions(+), 41 deletions(-) create mode 100644 webserver/src/service/utils.rs diff --git a/chain/src/main.rs b/chain/src/main.rs index e4f183bdb..42737e46d 100644 --- a/chain/src/main.rs +++ b/chain/src/main.rs @@ -157,6 +157,11 @@ async fn crawling_fn( let proposals = block.governance_proposal(next_governance_proposal_id); tracing::info!("Creating {} governance proposals...", proposals.len()); + let proposals_with_tally = + namada_service::query_tallies(&client, proposals) + .await + .into_rpc_error()?; + let proposals_votes = block.governance_votes(); tracing::info!("Creating {} governance votes...", proposals_votes.len()); @@ -187,7 +192,10 @@ async fn crawling_fn( balances, )?; - repository::gov::insert_proposals(transaction_conn, proposals)?; + repository::gov::insert_proposals( + transaction_conn, + proposals_with_tally, + )?; repository::gov::insert_votes( transaction_conn, proposals_votes, @@ -261,6 +269,17 @@ async fn initial_query( tracing::info!("Querying proposals..."); let proposals = query_all_proposals(client).await.into_rpc_error()?; + let proposals_with_tally = + namada_service::query_tallies(&client, proposals.clone()) + .await + .into_rpc_error()?; + + let proposals_votes = namada_service::query_all_votes( + &client, + proposals.iter().map(|p| p.id).collect(), + ) + .await + .into_rpc_error()?; let crawler_state = CrawlerState::new(block_height, epoch); @@ -275,7 +294,15 @@ async fn initial_query( balances, )?; - repository::gov::insert_proposals(transaction_conn, proposals)?; + repository::gov::insert_proposals( + transaction_conn, + proposals_with_tally, + )?; + + repository::gov::insert_votes( + transaction_conn, + proposals_votes, + )?; repository::pos::insert_bonds(transaction_conn, bonds)?; repository::pos::insert_unbonds(transaction_conn, unbonds)?; diff --git a/chain/src/repository/gov.rs b/chain/src/repository/gov.rs index b28d613a2..e8678fbe3 100644 --- a/chain/src/repository/gov.rs +++ b/chain/src/repository/gov.rs @@ -2,20 +2,20 @@ use anyhow::Context; use diesel::{PgConnection, RunQueryDsl}; use orm::governance_proposal::GovernanceProposalInsertDb; use orm::governance_votes::GovernanceProposalVoteInsertDb; -use shared::proposal::GovernanceProposal; +use shared::proposal::{GovernanceProposal, TallyType}; use shared::vote::GovernanceVote; pub fn insert_proposals( transaction_conn: &mut PgConnection, - proposals: Vec, + proposals: Vec<(GovernanceProposal, TallyType)>, ) -> anyhow::Result<()> { diesel::insert_into(orm::schema::governance_proposals::table) .values::<&Vec>( &proposals .into_iter() - .map(|proposal| { + .map(|(proposal, tally_type)| { GovernanceProposalInsertDb::from_governance_proposal( - proposal, + proposal, tally_type, ) }) .collect::>(), diff --git a/chain/src/services/namada.rs b/chain/src/services/namada.rs index 3283a04ff..fbe179ed2 100644 --- a/chain/src/services/namada.rs +++ b/chain/src/services/namada.rs @@ -20,9 +20,10 @@ use shared::balance::{Amount, Balance, Balances}; use shared::block::{BlockHeight, Epoch}; use shared::bond::{Bond, BondAddresses, Bonds}; use shared::id::Id; -use shared::proposal::GovernanceProposal; +use shared::proposal::{GovernanceProposal, TallyType}; use shared::unbond::{Unbond, UnbondAddresses, Unbonds}; use shared::utils::BalanceChange; +use shared::vote::{GovernanceVote, ProposalVoteKind}; use subtle_encoding::hex; use tendermint_rpc::HttpClient; @@ -416,6 +417,67 @@ pub async fn query_tx_code_hash( } } +pub async fn is_steward( + client: &HttpClient, + address: &Id, +) -> anyhow::Result { + let address = NamadaSdkAddress::from_str(&address.to_string()) + .context("Failed to parse address")?; + + let is_steward = rpc::is_steward(client, &address).await; + + Ok(is_steward) +} + +pub async fn query_tallies( + client: &HttpClient, + proposals: Vec, +) -> anyhow::Result> { + let proposals = futures::stream::iter(proposals) + .filter_map(|proposal| async move { + let is_steward = + is_steward(&client, &proposal.author).await.ok()?; + + let tally_type = TallyType::from(&proposal.r#type, is_steward); + + Some((proposal, tally_type)) + }) + .map(futures::future::ready) + .buffer_unordered(20) + .collect::>() + .await; + + anyhow::Ok(proposals) +} + +pub async fn query_all_votes( + client: &HttpClient, + proposals_ids: Vec, +) -> anyhow::Result> { + let votes = futures::stream::iter(proposals_ids) + .filter_map(|proposal_id| async move { + let votes = + rpc::query_proposal_votes(client, proposal_id).await.ok()?; + + let votes = votes + .into_iter() + .map(|vote| GovernanceVote { + proposal_id, + vote: ProposalVoteKind::from(vote.data), + address: Id::from(vote.delegator), + }) + .collect::>(); + + Some(votes) + }) + .map(futures::future::ready) + .buffer_unordered(20) + .collect::>() + .await; + + anyhow::Ok(votes.iter().cloned().flatten().collect()) +} + fn to_block_height(block_height: u32) -> NamadaSdkBlockHeight { NamadaSdkBlockHeight::from(block_height as u64) } diff --git a/orm/migrations/2024-05-08-130928_governance_proposals/down.sql b/orm/migrations/2024-05-08-130928_governance_proposals/down.sql index 1f67d28d0..9365198de 100644 --- a/orm/migrations/2024-05-08-130928_governance_proposals/down.sql +++ b/orm/migrations/2024-05-08-130928_governance_proposals/down.sql @@ -2,4 +2,5 @@ DROP TABLE governance_proposals; DROP TYPE GOVERNANCE_KIND; -DROP TYPE GOVERNANCE_RESULT; \ No newline at end of file +DROP TYPE GOVERNANCE_RESULT; +DROP TYPE GOVERNANCE_TALLY_TYPE; diff --git a/orm/migrations/2024-05-08-130928_governance_proposals/up.sql b/orm/migrations/2024-05-08-130928_governance_proposals/up.sql index 1d02569c3..386ec8148 100644 --- a/orm/migrations/2024-05-08-130928_governance_proposals/up.sql +++ b/orm/migrations/2024-05-08-130928_governance_proposals/up.sql @@ -1,11 +1,13 @@ CREATE TYPE GOVERNANCE_KIND AS ENUM ('pgf_steward', 'pgf_funding', 'default', 'default_with_wasm'); CREATE TYPE GOVERNANCE_RESULT AS ENUM ('passed', 'rejected', 'pending', 'unknown', 'voting_period'); +CREATE TYPE GOVERNANCE_TALLY_TYPE AS ENUM ('two_thirds', 'one_half_over_one_third', 'less_one_half_over_one_third_nay'); CREATE TABLE governance_proposals ( id INT PRIMARY KEY, content VARCHAR NOT NULL, data VARCHAR, kind GOVERNANCE_KIND NOT NULL, + tally_type GOVERNANCE_TALLY_TYPE NOT NULL, author VARCHAR NOT NULL, start_epoch INT NOT NULL, end_epoch INT NOT NULL, @@ -17,4 +19,4 @@ CREATE TABLE governance_proposals ( ); CREATE INDEX index_governance_proposals_kind ON governance_proposals USING HASH (kind); -CREATE INDEX index_governance_proposals_result ON governance_proposals USING HASH (result); \ No newline at end of file +CREATE INDEX index_governance_proposals_result ON governance_proposals USING HASH (result); diff --git a/orm/src/governance_proposal.rs b/orm/src/governance_proposal.rs index fb0cc5208..fa36d5f74 100644 --- a/orm/src/governance_proposal.rs +++ b/orm/src/governance_proposal.rs @@ -3,7 +3,7 @@ use diesel::{Insertable, Queryable, Selectable}; use serde::{Deserialize, Serialize}; use shared::proposal::{ GovernanceProposal, GovernanceProposalKind, GovernanceProposalResult, - GovernanceProposalStatus, + GovernanceProposalStatus, TallyType, }; use crate::schema::governance_proposals; @@ -28,6 +28,26 @@ impl From for GovernanceProposalKindDb { } } +#[derive(Debug, Clone, Serialize, Deserialize, diesel_derive_enum::DbEnum)] +#[ExistingTypePath = "crate::schema::sql_types::GovernanceTallyType"] +pub enum GovernanceProposalTallyTypeDb { + TwoThirds, + OneHalfOverOneThird, + LessOneHalfOverOneThirdNay, +} + +impl From for GovernanceProposalTallyTypeDb { + fn from(value: TallyType) -> Self { + match value { + TallyType::TwoThirds => Self::TwoThirds, + TallyType::OneHalfOverOneThird => Self::OneHalfOverOneThird, + TallyType::LessOneHalfOverOneThirdNay => { + Self::LessOneHalfOverOneThirdNay + } + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, diesel_derive_enum::DbEnum)] #[ExistingTypePath = "crate::schema::sql_types::GovernanceResult"] pub enum GovernanceProposalResultDb { @@ -57,6 +77,7 @@ pub struct GovernanceProposalDb { pub content: String, pub data: Option, pub kind: GovernanceProposalKindDb, + pub tally_type: GovernanceProposalTallyTypeDb, pub author: String, pub start_epoch: i32, pub end_epoch: i32, @@ -75,6 +96,7 @@ pub struct GovernanceProposalInsertDb { pub content: String, pub data: Option, pub kind: GovernanceProposalKindDb, + pub tally_type: GovernanceProposalTallyTypeDb, pub author: String, pub start_epoch: i32, pub end_epoch: i32, @@ -82,12 +104,16 @@ pub struct GovernanceProposalInsertDb { } impl GovernanceProposalInsertDb { - pub fn from_governance_proposal(proposal: GovernanceProposal) -> Self { + pub fn from_governance_proposal( + proposal: GovernanceProposal, + tally_type: TallyType, + ) -> Self { Self { id: proposal.id as i32, content: proposal.content, data: proposal.data, kind: proposal.r#type.into(), + tally_type: tally_type.into(), author: proposal.author.to_string(), start_epoch: proposal.voting_start_epoch as i32, end_epoch: proposal.voting_end_epoch as i32, diff --git a/orm/src/schema.rs b/orm/src/schema.rs index 499954050..e369545e9 100644 --- a/orm/src/schema.rs +++ b/orm/src/schema.rs @@ -9,6 +9,10 @@ pub mod sql_types { #[diesel(postgres_type(name = "governance_result"))] pub struct GovernanceResult; + #[derive(diesel::query_builder::QueryId, std::fmt::Debug, diesel::sql_types::SqlType)] + #[diesel(postgres_type(name = "governance_tally_type"))] + pub struct GovernanceTallyType; + #[derive(diesel::query_builder::QueryId, std::fmt::Debug, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "vote_kind"))] pub struct VoteKind; @@ -50,6 +54,7 @@ diesel::table! { diesel::table! { use diesel::sql_types::*; use super::sql_types::GovernanceKind; + use super::sql_types::GovernanceTallyType; use super::sql_types::GovernanceResult; governance_proposals (id) { @@ -57,6 +62,7 @@ diesel::table! { content -> Varchar, data -> Nullable, kind -> GovernanceKind, + tally_type -> GovernanceTallyType, author -> Varchar, start_epoch -> Int4, end_epoch -> Int4, diff --git a/shared/src/proposal.rs b/shared/src/proposal.rs index afecc7a54..edfb299ce 100644 --- a/shared/src/proposal.rs +++ b/shared/src/proposal.rs @@ -218,3 +218,43 @@ impl Distribution for Standard { } } } + +pub enum TallyType { + TwoThirds, + OneHalfOverOneThird, + LessOneHalfOverOneThirdNay, +} + +// TODO: copied from namada for time being +impl TallyType { + pub fn from( + proposal_type: &GovernanceProposalKind, + is_steward: bool, + ) -> Self { + match (proposal_type, is_steward) { + (GovernanceProposalKind::Default, _) => TallyType::TwoThirds, + (GovernanceProposalKind::DefaultWithWasm, _) => { + TallyType::TwoThirds + } + (GovernanceProposalKind::PgfSteward, _) => { + TallyType::OneHalfOverOneThird + } + (GovernanceProposalKind::PgfFunding, true) => { + TallyType::LessOneHalfOverOneThirdNay + } + (GovernanceProposalKind::PgfFunding, false) => { + TallyType::OneHalfOverOneThird + } + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> TallyType { + match rng.gen_range(0..=2) { + 0 => TallyType::TwoThirds, + 1 => TallyType::OneHalfOverOneThird, + _ => TallyType::LessOneHalfOverOneThirdNay, + } + } +} diff --git a/swagger.yml b/swagger.yml index 372a51c9a..69aae38e9 100644 --- a/swagger.yml +++ b/swagger.yml @@ -159,9 +159,7 @@ paths: content: application/json: schema: - type: array - items: - $ref: '#/components/schemas/VotingPower' + $ref: '#/components/schemas/VotingPower' /api/v1/gov/proposal: get: summary: Get a list of governance proposals @@ -364,7 +362,7 @@ components: type: string Proposal: type: object - required: [id, content, type, author, startEpoch, endEpoch, activationEpoch, startTime, endTime, currentTime, status, yayVotes, nayVotes, abstainVotes] + required: [id, content, type, author, startEpoch, endEpoch, activationEpoch, startTime, endTime, currentTime, status, yayVotes, nayVotes, abstainVotes, tallyType] properties: id: type: integer @@ -372,7 +370,10 @@ components: type: string type: type: string - enum: [default, default_with_wasm, pgf_steward, pgf_funding] + enum: [default, defaultWithWasm, pgfSteward, pgfFunding] + tallyType: + type: string + enum: [twoThirds, oneHalfOverOneThird, lessOneHalfOverOneThirdNay] data: type: string author: @@ -393,11 +394,11 @@ components: type: string enum: [pending, voting, passed, rejected] yayVotes: - type: integer + type: string nayVotes: - type: integer + type: string abstainVotes: - type: integer + type: string Vote: type: object required: [proposal_id, vote, voterAddress] @@ -453,11 +454,12 @@ components: format: float minimum: 0 withdrawEpoch: - type: integer + type: integer VotingPower: type: object + required: [totalVotingPower] properties: - votingPower: + totalVotingPower: type: integer Balance: type: object diff --git a/webserver/src/response/governance.rs b/webserver/src/response/governance.rs index 2ba24e4e8..95636dbca 100644 --- a/webserver/src/response/governance.rs +++ b/webserver/src/response/governance.rs @@ -3,6 +3,7 @@ use std::fmt::Display; use namada_core::time::DateTimeUtc; use orm::governance_proposal::{ GovernanceProposalDb, GovernanceProposalKindDb, GovernanceProposalResultDb, + GovernanceProposalTallyTypeDb, }; use orm::governance_votes::{GovernanceProposalVoteDb, GovernanceVoteKindDb}; use serde::{Deserialize, Serialize}; @@ -27,6 +28,28 @@ impl Display for ProposalType { } } +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum TallyType { + TwoThirds, + OneHalfOverOneThird, + LessOneHalfOverOneThirdNay, +} + +impl Display for TallyType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TallyType::TwoThirds => write!(f, "two_thirds"), + TallyType::OneHalfOverOneThird => { + write!(f, "one_half_over_one_third") + } + TallyType::LessOneHalfOverOneThirdNay => { + write!(f, "less_one_half_over_one_third_nay") + } + } + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum VoteType { @@ -63,6 +86,7 @@ pub struct Proposal { pub id: u64, pub content: String, pub r#type: ProposalType, + pub tally_type: TallyType, pub data: Option, pub author: String, pub start_epoch: u64, @@ -72,9 +96,9 @@ pub struct Proposal { pub end_time: i64, pub current_time: i64, pub status: ProposalStatus, - pub yay_votes: u64, - pub nay_votes: u64, - pub abstain_votes: u64, + pub yay_votes: String, + pub nay_votes: String, + pub abstain_votes: String, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -120,6 +144,17 @@ impl Proposal { ProposalType::DefaultWithWasm } }, + tally_type: match value.tally_type { + GovernanceProposalTallyTypeDb::TwoThirds => { + TallyType::TwoThirds + } + GovernanceProposalTallyTypeDb::OneHalfOverOneThird => { + TallyType::OneHalfOverOneThird + } + GovernanceProposalTallyTypeDb::LessOneHalfOverOneThirdNay => { + TallyType::LessOneHalfOverOneThirdNay + } + }, data: value.data, author: value.author, start_epoch: value.start_epoch as u64, @@ -141,12 +176,9 @@ impl Proposal { ProposalStatus::Voting } }, - yay_votes: value.yay_votes.parse::().unwrap_or_default(), - nay_votes: value.nay_votes.parse::().unwrap_or_default(), - abstain_votes: value - .abstain_votes - .parse::() - .unwrap_or_default(), + yay_votes: value.yay_votes, + nay_votes: value.nay_votes, + abstain_votes: value.abstain_votes, } } } diff --git a/webserver/src/service/balance.rs b/webserver/src/service/balance.rs index 6430b7cf6..7f2f519b6 100644 --- a/webserver/src/service/balance.rs +++ b/webserver/src/service/balance.rs @@ -1,10 +1,10 @@ -use namada_core::token::Amount; - use crate::appstate::AppState; use crate::error::balance::BalanceError; use crate::repository::balance::{BalanceRepo, BalanceRepoTrait}; use crate::response::balance::AddressBalance; +use super::utils::raw_amount_to_nam; + #[derive(Clone)] pub struct BalanceService { pub balance_repo: BalanceRepo, @@ -33,9 +33,7 @@ impl BalanceService { .cloned() .map(|balance| AddressBalance { token_address: balance.token, - balance: Amount::from_str(balance.raw_amount, 0) - .unwrap() - .to_string_native(), + balance: raw_amount_to_nam(balance.raw_amount), }) .collect(); diff --git a/webserver/src/service/mod.rs b/webserver/src/service/mod.rs index aa95e0d78..5310da139 100644 --- a/webserver/src/service/mod.rs +++ b/webserver/src/service/mod.rs @@ -2,3 +2,4 @@ pub mod balance; pub mod chain; pub mod governance; pub mod pos; +pub mod utils; diff --git a/webserver/src/service/pos.rs b/webserver/src/service/pos.rs index 91cd6a7b1..76a4abead 100644 --- a/webserver/src/service/pos.rs +++ b/webserver/src/service/pos.rs @@ -3,6 +3,8 @@ use crate::error::pos::PoSError; use crate::repository::pos::{PosRepository, PosRepositoryTrait}; use crate::response::pos::{Bond, Reward, Unbond, ValidatorWithId, Withdraw}; +use super::utils::raw_amount_to_nam; + #[derive(Clone)] pub struct PosService { pos_repo: PosRepository, @@ -79,7 +81,16 @@ impl PosService { ); } } - Ok(bonds) + + let denominated_bonds: Vec = bonds + .iter() + .cloned() + .map(|bond| Bond { + amount: raw_amount_to_nam(bond.amount), + ..bond + }) + .collect(); + Ok(denominated_bonds) } pub async fn get_unbonds_by_address( @@ -107,7 +118,15 @@ impl PosService { ); } } - Ok(unbonds) + let denominated_unbonds: Vec = unbonds + .iter() + .cloned() + .map(|unbond| Unbond { + amount: raw_amount_to_nam(unbond.amount), + ..unbond + }) + .collect(); + Ok(denominated_unbonds) } pub async fn get_withdraws_by_address( @@ -121,14 +140,14 @@ impl PosService { .await .map_err(PoSError::Database)?; - let mut unbonds = vec![]; + let mut withdraws = vec![]; for db_unbond in db_unbonds { let db_validator = self .pos_repo .find_validator_by_id(db_unbond.validator_id) .await; if let Ok(Some(db_validator)) = db_validator { - unbonds.push(Withdraw::from(db_unbond.clone(), db_validator)); + withdraws.push(Withdraw::from(db_unbond.clone(), db_validator)); } else { tracing::error!( "Couldn't find validator with id {} in bond query", @@ -136,7 +155,15 @@ impl PosService { ); } } - Ok(unbonds) + let denominated_withdraw: Vec = withdraws + .iter() + .cloned() + .map(|withdraw| Withdraw { + amount: raw_amount_to_nam(withdraw.amount), + ..withdraw + }) + .collect(); + Ok(denominated_withdraw) } pub async fn get_rewards_by_address( @@ -164,9 +191,18 @@ impl PosService { ); } } - Ok(rewards) + let denominated_rewards: Vec = rewards + .iter() + .cloned() + .map(|reward| Reward { + amount: raw_amount_to_nam(reward.amount), + ..reward + }) + .collect(); + Ok(denominated_rewards) } + // TODO: maybe remove object(struct) instead pub async fn get_total_voting_power(&self) -> Result { let total_voting_power_db = self .pos_repo diff --git a/webserver/src/service/utils.rs b/webserver/src/service/utils.rs new file mode 100644 index 000000000..dc1da5d30 --- /dev/null +++ b/webserver/src/service/utils.rs @@ -0,0 +1,7 @@ +use namada_core::token::Amount; + +pub fn raw_amount_to_nam(raw_amount: String) -> String { + Amount::from_str(raw_amount, 0) + .expect("raw_amount is not a valid string") + .to_string_native() +}