diff --git a/Cargo.toml b/Cargo.toml index 475e2a186..a4d7a199f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,11 +40,12 @@ chrono = { version = "0.4.30", features = ["serde"] } async-trait = "0.1.73" anyhow = "1.0.75" namada_core = { git = "https://github.com/anoma/namada", tag = "v0.38.1" } -namada_sdk = { git = "https://github.com/anoma/namada", tag = "v0.38.1" } +namada_sdk = { git = "https://github.com/anoma/namada", tag = "v0.38.1", default-features = false, features = ["tendermint-rpc", "std", "async-send", "download-params", "rand"] } namada_tx = { git = "https://github.com/anoma/namada", tag = "v0.38.1" } namada_governance = { git = "https://github.com/anoma/namada", tag = "v0.38.1" } namada_ibc = { git = "https://github.com/anoma/namada", tag = "v0.38.1" } namada_token = { git = "https://github.com/anoma/namada", tag = "v0.38.1" } +namada_parameters = { git = "https://github.com/anoma/namada", tag = "v0.38.1" } tendermint = "0.36.0" tendermint-config = "0.36.0" tendermint-rpc = { version = "0.36.0", features = ["http-client"] } diff --git a/chain/src/main.rs b/chain/src/main.rs index e4f183bdb..d0c88c63a 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()); @@ -172,6 +177,12 @@ async fn crawling_fn( .into_rpc_error()?; tracing::info!("Updating unbonds for {} addresses", unbonds.len()); + let revealed_pks = block.revealed_pks(); + tracing::info!( + "Updating revealed pks for {} addresses", + revealed_pks.len() + ); + let metadata_change = block.validator_metadata(); let reward_claimers = block.pos_rewards(); @@ -187,7 +198,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, @@ -196,11 +210,6 @@ async fn crawling_fn( repository::pos::insert_bonds(transaction_conn, bonds)?; repository::pos::insert_unbonds(transaction_conn, unbonds)?; - repository::crawler::insert_crawler_state( - transaction_conn, - crawler_state, - )?; - repository::pos::delete_claimed_rewards( transaction_conn, reward_claimers, @@ -211,6 +220,16 @@ async fn crawling_fn( metadata_change, )?; + repository::revealed_pk::insert_revealed_pks( + transaction_conn, + revealed_pks, + )?; + + repository::crawler::insert_crawler_state( + transaction_conn, + crawler_state, + )?; + anyhow::Ok(()) }) }) @@ -261,6 +280,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 +305,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/repository/mod.rs b/chain/src/repository/mod.rs index 0de5524b6..c811ef582 100644 --- a/chain/src/repository/mod.rs +++ b/chain/src/repository/mod.rs @@ -2,3 +2,4 @@ pub mod balance; pub mod crawler; pub mod gov; pub mod pos; +pub mod revealed_pk; diff --git a/chain/src/repository/pos.rs b/chain/src/repository/pos.rs index 5d676b891..02994063d 100644 --- a/chain/src/repository/pos.rs +++ b/chain/src/repository/pos.rs @@ -110,6 +110,7 @@ pub fn update_validator_metadata( for metadata in metadata_change { let metadata_change_db = ValidatorUpdateMetadataDb { commission: metadata.commission, + name: metadata.name, email: metadata.email, website: metadata.website, description: metadata.description, diff --git a/chain/src/repository/revealed_pk.rs b/chain/src/repository/revealed_pk.rs new file mode 100644 index 000000000..69c433aa2 --- /dev/null +++ b/chain/src/repository/revealed_pk.rs @@ -0,0 +1,22 @@ +use anyhow::Context; +use diesel::{PgConnection, RunQueryDsl}; +use orm::{revealed_pk::RevealedPkInsertDb, schema::revealed_pk}; +use shared::{id::Id, public_key::PublicKey}; + +pub fn insert_revealed_pks( + transaction_conn: &mut PgConnection, + revealed_pks: Vec<(PublicKey, Id)>, +) -> anyhow::Result<()> { + diesel::insert_into(revealed_pk::table) + .values::<&Vec>( + &revealed_pks + .into_iter() + .map(|(pk, address)| RevealedPkInsertDb::from(pk, address)) + .collect::>(), + ) + .on_conflict_do_nothing() + .execute(transaction_conn) + .context("Failed to update balances in db")?; + + anyhow::Ok(()) +} diff --git a/chain/src/services/namada.rs b/chain/src/services/namada.rs index 3283a04ff..b7e2483c2 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,66 @@ 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().flatten().cloned().collect()) +} + fn to_block_height(block_height: u32) -> NamadaSdkBlockHeight { NamadaSdkBlockHeight::from(block_height as u64) } diff --git a/docker-compose.yml b/docker-compose.yml index 0b7e1f0fe..b2cef3a10 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,10 +65,11 @@ services: environment: DATABASE_URL: postgres://postgres:password@postgres:5435/namada-indexer CACHE_URL: redis://dragonfly:6379 + TENDERMINT_URL: http://localhost:27657 healthcheck: test: curl --fail http://localhost:5000/health || exit 1 interval: 15s timeout: 10s retries: 3 start_period: 10s - \ No newline at end of file + diff --git a/orm/migrations/.keep b/orm/migrations/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/orm/migrations/2024-04-30-081808_init_validators/up.sql b/orm/migrations/2024-04-30-081808_init_validators/up.sql index 7f9959971..84911bba1 100644 --- a/orm/migrations/2024-04-30-081808_init_validators/up.sql +++ b/orm/migrations/2024-04-30-081808_init_validators/up.sql @@ -6,6 +6,7 @@ CREATE TABLE validators ( voting_power INT NOT NULL, max_commission VARCHAR NOT NULL, commission VARCHAR NOT NULL, + name VARCHAR, email VARCHAR, website VARCHAR, description VARCHAR, 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/migrations/2024-06-03-084149_init_revealed_pk/down.sql b/orm/migrations/2024-06-03-084149_init_revealed_pk/down.sql new file mode 100644 index 000000000..c73c0034e --- /dev/null +++ b/orm/migrations/2024-06-03-084149_init_revealed_pk/down.sql @@ -0,0 +1,3 @@ +-- This file should undo anything in `up.sql` + +DROP TABLE IF EXISTS revealed_pk; diff --git a/orm/migrations/2024-06-03-084149_init_revealed_pk/up.sql b/orm/migrations/2024-06-03-084149_init_revealed_pk/up.sql new file mode 100644 index 000000000..28cffbfdd --- /dev/null +++ b/orm/migrations/2024-06-03-084149_init_revealed_pk/up.sql @@ -0,0 +1,9 @@ +-- Your SQL goes here + +CREATE TABLE revealed_pk ( + id SERIAL PRIMARY KEY, + address VARCHAR NOT NULL, + pk VARCHAR NOT NULL +); + +ALTER TABLE revealed_pk ADD UNIQUE (address, pk); diff --git a/orm/src/bond.rs b/orm/src/bond.rs index 7d3f6f4d4..5c28a2180 100644 --- a/orm/src/bond.rs +++ b/orm/src/bond.rs @@ -1,6 +1,10 @@ -use diesel::{Insertable, Queryable, Selectable}; +use diesel::{ + associations::Associations, Identifiable, Insertable, Queryable, Selectable, +}; use shared::bond::Bond; +use crate::validators::ValidatorDb; + use crate::schema::bonds; #[derive(Insertable, Clone, Queryable, Selectable)] @@ -12,7 +16,16 @@ pub struct BondInsertDb { pub raw_amount: String, } -pub type BondDb = BondInsertDb; +#[derive(Identifiable, Clone, Queryable, Selectable, Associations)] +#[diesel(table_name = bonds)] +#[diesel(check_for_backend(diesel::pg::Pg))] +#[diesel(belongs_to(ValidatorDb, foreign_key = validator_id))] +pub struct BondDb { + pub id: i32, + pub address: String, + pub validator_id: i32, + pub raw_amount: String, +} impl BondInsertDb { pub fn from_bond(bond: Bond, validator_id: i32) -> Self { 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/lib.rs b/orm/src/lib.rs index 078f8f44f..3fb61687a 100644 --- a/orm/src/lib.rs +++ b/orm/src/lib.rs @@ -6,6 +6,7 @@ pub mod governance_proposal; pub mod governance_votes; pub mod migrations; pub mod pos_rewards; +pub mod revealed_pk; pub mod schema; pub mod unbond; pub mod validators; diff --git a/orm/src/revealed_pk.rs b/orm/src/revealed_pk.rs new file mode 100644 index 000000000..618068a3b --- /dev/null +++ b/orm/src/revealed_pk.rs @@ -0,0 +1,23 @@ +use diesel::{Insertable, Queryable, Selectable}; +use shared::{id::Id, public_key::PublicKey}; + +use crate::schema::revealed_pk; + +#[derive(Insertable, Clone, Queryable, Selectable)] +#[diesel(table_name = revealed_pk)] +#[diesel(check_for_backend(diesel::pg::Pg))] +pub struct RevealedPkInsertDb { + pub pk: String, + pub address: String, +} + +pub type RevealedPkDb = RevealedPkInsertDb; + +impl RevealedPkInsertDb { + pub fn from(pk: PublicKey, address: Id) -> Self { + Self { + pk: pk.0, + address: address.to_string(), + } + } +} diff --git a/orm/src/schema.rs b/orm/src/schema.rs index 15c38ccd2..00a74717b 100644 --- a/orm/src/schema.rs +++ b/orm/src/schema.rs @@ -1,27 +1,19 @@ // @generated automatically by Diesel CLI. pub mod sql_types { - #[derive( - diesel::query_builder::QueryId, - std::fmt::Debug, - diesel::sql_types::SqlType, - )] + #[derive(diesel::query_builder::QueryId, std::fmt::Debug, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "governance_kind"))] pub struct GovernanceKind; - #[derive( - diesel::query_builder::QueryId, - std::fmt::Debug, - diesel::sql_types::SqlType, - )] + #[derive(diesel::query_builder::QueryId, std::fmt::Debug, diesel::sql_types::SqlType)] #[diesel(postgres_type(name = "governance_result"))] pub struct GovernanceResult; - #[derive( - diesel::query_builder::QueryId, - std::fmt::Debug, - diesel::sql_types::SqlType, - )] + #[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; } @@ -62,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) { @@ -69,6 +62,7 @@ diesel::table! { content -> Varchar, data -> Nullable, kind -> GovernanceKind, + tally_type -> GovernanceTallyType, author -> Varchar, start_epoch -> Int4, end_epoch -> Int4, @@ -101,6 +95,14 @@ diesel::table! { } } +diesel::table! { + revealed_pk (id) { + id -> Int4, + address -> Varchar, + pk -> Varchar, + } +} + diesel::table! { unbonds (id) { id -> Int4, @@ -118,6 +120,7 @@ diesel::table! { voting_power -> Int4, max_commission -> Varchar, commission -> Varchar, + name -> Nullable, email -> Nullable, website -> Nullable, description -> Nullable, @@ -139,6 +142,7 @@ diesel::allow_tables_to_appear_in_same_query!( governance_proposals, governance_votes, pos_rewards, + revealed_pk, unbonds, validators, ); diff --git a/orm/src/validators.rs b/orm/src/validators.rs index e9ab9e363..00c3aba69 100644 --- a/orm/src/validators.rs +++ b/orm/src/validators.rs @@ -1,12 +1,12 @@ use std::str::FromStr; -use diesel::{AsChangeset, Insertable, Queryable, Selectable}; +use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use serde::Serialize; use shared::validator::Validator; use crate::schema::validators; -#[derive(Serialize, Queryable, Selectable, Clone)] +#[derive(Identifiable, Serialize, Queryable, Selectable, Clone, Debug)] #[diesel(table_name = validators)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct ValidatorDb { @@ -15,6 +15,7 @@ pub struct ValidatorDb { pub voting_power: i32, pub max_commission: String, pub commission: String, + pub name: Option, pub email: Option, pub website: Option, pub description: Option, @@ -37,6 +38,7 @@ pub struct ValidatorInsertDb { #[diesel(check_for_backend(diesel::pg::Pg))] pub struct ValidatorUpdateMetadataDb { pub commission: Option, + pub name: Option, pub email: Option, pub website: Option, pub description: Option, diff --git a/pos/src/services/namada.rs b/pos/src/services/namada.rs index 46a2ca684..691fac893 100644 --- a/pos/src/services/namada.rs +++ b/pos/src/services/namada.rs @@ -63,6 +63,7 @@ pub async fn get_validator_set_at_epoch( voting_power: voting_power.to_string_native(), max_commission, commission, + name: None, email: None, description: None, website: None, diff --git a/seeder/src/main.rs b/seeder/src/main.rs index 66e8a62c6..17a8865da 100644 --- a/seeder/src/main.rs +++ b/seeder/src/main.rs @@ -21,7 +21,9 @@ use seeder::state::AppState; use shared::balance::Balance; use shared::bond::Bond; use shared::error::{AsDbError, ContextDbInteractError, MainError}; -use shared::proposal::{GovernanceProposal, GovernanceProposalStatus}; +use shared::proposal::{ + GovernanceProposal, GovernanceProposalStatus, TallyType, +}; use shared::rewards::Reward; use shared::unbond::Unbond; use shared::validator::Validator; @@ -58,6 +60,15 @@ async fn main() -> anyhow::Result<(), MainError> { .map(GovernanceProposal::fake) .collect::>(); + let governance_proposals_with_tally = governance_proposals + .iter() + .cloned() + .map(|proposal| { + let tally_type = TallyType::fake(); + (proposal, tally_type) + }) + .collect::>(); + let governance_proposals_status = (0..config.total_proposals) .map(GovernanceProposalStatus::fake) .collect::>(); @@ -149,10 +160,10 @@ async fn main() -> anyhow::Result<(), MainError> { diesel::insert_into(governance_proposals::table) .values::<&Vec>( - &governance_proposals + &governance_proposals_with_tally .into_iter() - .map(|proposal| { - GovernanceProposalInsertDb::from_governance_proposal(proposal) + .map(|(proposal, tally_type)| { + GovernanceProposalInsertDb::from_governance_proposal(proposal, tally_type) }) .collect::>(), ) diff --git a/shared/src/block.rs b/shared/src/block.rs index 1f08953f9..c4c7527b9 100644 --- a/shared/src/block.rs +++ b/shared/src/block.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashSet}; use std::str::FromStr; use namada_sdk::borsh::BorshDeserialize; +use namada_sdk::key::common::PublicKey as NamadaPublicKey; use namada_tx::data::pos; use subtle_encoding::hex; use tendermint_rpc::endpoint::block::Response as TendermintBlockResponse; @@ -12,6 +13,7 @@ use crate::checksums::Checksums; use crate::header::BlockHeader; use crate::id::Id; use crate::proposal::{GovernanceProposal, GovernanceProposalKind}; +use crate::public_key::PublicKey; use crate::transaction::{Transaction, TransactionKind}; use crate::unbond::UnbondAddresses; use crate::utils::BalanceChange; @@ -452,6 +454,7 @@ impl Block { commission: metadata_change_data .commission_rate .map(|c| c.to_string()), + name: metadata_change_data.name, email: metadata_change_data.email, description: metadata_change_data.description, website: metadata_change_data.website, @@ -473,6 +476,7 @@ impl Block { commission: Some( commission_change.new_rate.to_string(), ), + name: None, email: None, description: None, website: None, @@ -484,4 +488,22 @@ impl Block { }) .collect() } + + pub fn revealed_pks(&self) -> Vec<(PublicKey, Id)> { + self.transactions + .iter() + .filter_map(|tx| match &tx.kind { + TransactionKind::RevealPk(data) => { + let namada_public_key = + NamadaPublicKey::try_from_slice(data).unwrap(); + + Some(( + PublicKey::from(namada_public_key.clone()), + Id::from(namada_public_key), + )) + } + _ => None, + }) + .collect() + } } diff --git a/shared/src/lib.rs b/shared/src/lib.rs index 82a6cc8c3..d97ebeca4 100644 --- a/shared/src/lib.rs +++ b/shared/src/lib.rs @@ -9,6 +9,7 @@ pub mod error; pub mod header; pub mod id; pub mod proposal; +pub mod public_key; pub mod rewards; pub mod transaction; pub mod unbond; diff --git a/shared/src/proposal.rs b/shared/src/proposal.rs index afecc7a54..6cd55d204 100644 --- a/shared/src/proposal.rs +++ b/shared/src/proposal.rs @@ -218,3 +218,47 @@ impl Distribution for Standard { } } } + +pub enum TallyType { + TwoThirds, + OneHalfOverOneThird, + LessOneHalfOverOneThirdNay, +} + +// TODO: copied from namada for time being +impl TallyType { + pub fn fake() -> Self { + rand::random() + } + + 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/shared/src/public_key.rs b/shared/src/public_key.rs new file mode 100644 index 000000000..53b3fa223 --- /dev/null +++ b/shared/src/public_key.rs @@ -0,0 +1,9 @@ +use namada_sdk::key::common::PublicKey as NamadaPublicKey; + +pub struct PublicKey(pub String); + +impl From for PublicKey { + fn from(pk: NamadaPublicKey) -> Self { + PublicKey(pk.to_string()) + } +} diff --git a/shared/src/transaction.rs b/shared/src/transaction.rs index f350ec42c..65131eca7 100644 --- a/shared/src/transaction.rs +++ b/shared/src/transaction.rs @@ -24,6 +24,7 @@ pub enum TransactionKind { InitProposal(Vec), MetadataChange(Vec), CommissionChange(Vec), + RevealPk(Vec), Unknown, } @@ -60,6 +61,7 @@ impl TransactionKind { "tx_commission_change" => { TransactionKind::CommissionChange(data.to_vec()) } + "tx_reveal_pk" => TransactionKind::RevealPk(data.to_vec()), _ => TransactionKind::Unknown, } } diff --git a/shared/src/validator.rs b/shared/src/validator.rs index a62ca9f3c..7b5a97e30 100644 --- a/shared/src/validator.rs +++ b/shared/src/validator.rs @@ -19,6 +19,7 @@ pub struct Validator { pub voting_power: VotingPower, pub max_commission: String, pub commission: String, + pub name: Option, pub email: Option, pub description: Option, pub website: Option, @@ -30,6 +31,7 @@ pub struct Validator { pub struct ValidatorMetadataChange { pub address: Id, pub commission: Option, + pub name: Option, pub email: Option, pub description: Option, pub website: Option, @@ -47,9 +49,10 @@ impl Validator { let commission = ((0..100).fake::() as f64 / 100_f64).to_string(); let email = Some(SafeEmail().fake()); let description: Option = CatchPhase().fake(); + let name = Some(CompanyName().fake::()); let website: Option = Some(format!( "{}.{}", - CompanyName().fake::(), + name.clone().unwrap(), DomainSuffix().fake::() )); let discord_handler: Option = Username().fake(); @@ -59,6 +62,7 @@ impl Validator { voting_power, max_commission, commission, + name, email, description, website, diff --git a/swagger.yml b/swagger.yml index 319711129..e082b76de 100644 --- a/swagger.yml +++ b/swagger.yml @@ -16,6 +16,13 @@ paths: description: Health check /api/v1/pos/validator: get: + parameters: + - in: query + name: page + schema: + type: integer + minimum: 1 + description: Pagination parameter responses: '200': description: A list of validator. @@ -23,26 +30,14 @@ paths: application/json: schema: type: object + required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/Validator' pagination: - type: object - properties: - page: - type: integer - minimum: 0 - per_page: - type: integer - minimum: 0 - total_pages: - type: integer - minimum: 0 - total_items: - type: integer - minimum: 0 + $ref: '#/components/schemas/Pagination' /api/v1/pos/reward/{address}: get: summary: Get all the rewards for an address @@ -134,9 +129,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 @@ -160,26 +153,14 @@ paths: application/json: schema: type: object + required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/Proposal' pagination: - type: object - properties: - page: - type: integer - minimum: 0 - per_page: - type: integer - minimum: 0 - total_pages: - type: integer - minimum: 0 - total_items: - type: integer - minimum: 0 + $ref: '#/components/schemas/Pagination' /api/v1/gov/search/{text}: get: summary: Get a list of governance proposals matching a text in the title @@ -204,26 +185,14 @@ paths: application/json: schema: type: object + required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/Proposal' pagination: - type: object - properties: - page: - type: integer - minimum: 0 - per_page: - type: integer - minimum: 0 - total_pages: - type: integer - minimum: 0 - total_items: - type: integer - minimum: 0 + $ref: '#/components/schemas/Pagination' /api/v1/gov/proposal/{id}: get: summary: Get a governance proposal by proposal id @@ -260,26 +229,14 @@ paths: application/json: schema: type: object + required: [data, pagination] properties: data: type: array items: $ref: '#/components/schemas/Vote' pagination: - type: object - properties: - page: - type: integer - minimum: 0 - per_page: - type: integer - minimum: 0 - total_pages: - type: integer - minimum: 0 - total_items: - type: integer - minimum: 0 + $ref: '#/components/schemas/Pagination' /api/v1/gov/proposal/{id}/votes/{address}: get: summary: Get all the votes for a governance proposal from an address @@ -306,6 +263,25 @@ paths: type: array items: $ref: '#/components/schemas/Vote' + /api/v1/gov/voter/{address}/votes: + get: + summary: Get all the votes from a voter + parameters: + - in: path + name: address + schema: + type: string + required: true + description: The voter address + responses: + '200': + description: A list of votes. + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Vote' /api/v1/account/{address}: get: summary: Get the all the tokens balances of an address @@ -322,38 +298,72 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/Balance' + type: array + items: + $ref: '#/components/schemas/Balance' + + /api/v1/revealed_public_key/{address}: + get: + summary: Get revealed public key for an address if exists + parameters: + - in: path + name: address + schema: + type: string + required: true + description: The address account + responses: + '200': + description: Revealed public key. + content: + application/json: + schema: + $ref: '#/components/schemas/RevealedPk' + + /api/v1/gas_table: + get: + summary: Get the gas table + responses: + '200': + description: Gas table + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Balance' components: schemas: Validator: type: object - required: [id, address, name, voting_power, max_commission, commission] + required: [validatorId, address, name, votingPower, maxCommission, commission] properties: - id: + validatorId: type: integer address: type: string - name: - type: string - voting_power: + votingPower: type: string - max_commission: + maxCommission: type: string commission: type: string + name: + type: string email: type: string website: type: string description: type: string - discord_handle: + discordHandle: type: string avatar: type: string Proposal: type: object + required: [id, content, type, author, startEpoch, endEpoch, activationEpoch, startTime, endTime, currentTime, status, yayVotes, nayVotes, abstainVotes, tallyType] properties: id: type: integer @@ -361,6 +371,10 @@ components: type: string type: type: string + enum: [default, defaultWithWasm, pgfSteward, pgfFunding] + tallyType: + type: string + enum: [twoThirds, oneHalfOverOneThird, lessOneHalfOverOneThirdNay] data: type: string author: @@ -371,30 +385,36 @@ components: type: integer activationEpoch: type: integer + startTime: + type: integer + endTime: + type: integer + currentTime: + type: integer status: type: string enum: [pending, voting, passed, rejected] yayVotes: - type: integer + type: string nayVotes: - type: integer - abstrainVotes: - type: integer + type: string + abstainVotes: + type: string Vote: type: object + required: [proposal_id, vote, voterAddress] properties: proposal_id: type: integer vote: type: string - enum: [yay, nay, abstrain] + enum: [yay, nay, abstain] voterAddress: type: string Reward: type: object properties: validator: - type: object $ref: '#/components/schemas/Validator' amount: type: number @@ -402,9 +422,9 @@ components: minimum: 0 Bond: type: object + required: [validator, amount] properties: validator: - type: object $ref: '#/components/schemas/Validator' amount: type: number @@ -412,39 +432,67 @@ components: minimum: 0 Unbond: type: object + required: [validator, amount, withdrawEpoch] properties: validator: - type: object $ref: '#/components/schemas/Validator' amount: type: number format: float minimum: 0 - withdrawEpoch: - type: integer + withdrawEpoch: + type: number Withdraw: type: object + required: [amount, withdrawEpoch] properties: validator: - type: object $ref: '#/components/schemas/Validator' amount: type: number format: float minimum: 0 - withdrawEpoch: - type: integer + withdrawEpoch: + type: number VotingPower: type: object + required: [totalVotingPower] properties: - votingPower: + totalVotingPower: type: integer Balance: type: object + required: [tokenAddress, balance] properties: - token: + tokenAddress: type: string balance: - type: number - format: float + type: string + Pagination: + type: object + properties: + page: + type: integer + minimum: 0 + per_page: + type: integer + minimum: 0 + total_pages: + type: integer minimum: 0 + total_items: + type: integer + minimum: 0 + RevealedPk: + type: object + properties: + publicKey: + type: string + GasCost: + type: object + properties: + tokenAddress: + type: string + amount: + type: string + diff --git a/webserver/Cargo.toml b/webserver/Cargo.toml index 6b1ccc1bb..f67880ecb 100644 --- a/webserver/Cargo.toml +++ b/webserver/Cargo.toml @@ -40,8 +40,10 @@ orm.workspace = true diesel.workspace = true futures.workspace = true tokio-stream.workspace = true -# shared.workspace = true +namada_core.workspace = true +namada_sdk.workspace = true +namada_parameters.workspace = true deadpool-redis = "0.13.0" [build-dependencies] -vergen = { version = "8.0.0", features = ["build", "git", "gitcl"] } \ No newline at end of file +vergen = { version = "8.0.0", features = ["build", "git", "gitcl"] } diff --git a/webserver/run.sh b/webserver/run.sh index 07d58ef40..a61a5bd39 100755 --- a/webserver/run.sh +++ b/webserver/run.sh @@ -1 +1 @@ -cargo run -- --database-url postgres://postgres:password@0.0.0.0:5435/namada-indexer --cache-url redis://redis@0.0.0.0:6379 +cargo run -- --database-url postgres://postgres:password@0.0.0.0:5435/namada-indexer --cache-url redis://redis@0.0.0.0:6379 --tendermint-url http://127.0.0.1:27657 diff --git a/webserver/src/app.rs b/webserver/src/app.rs index 17b2c9350..fd4003489 100644 --- a/webserver/src/app.rs +++ b/webserver/src/app.rs @@ -9,6 +9,7 @@ use axum::routing::get; use axum::{BoxError, Json, Router}; use axum_trace_id::SetTraceIdLayer; use lazy_static::lazy_static; +use namada_sdk::tendermint_rpc::HttpClient; use serde_json::json; use tower::buffer::BufferLayer; use tower::limit::RateLimitLayer; @@ -19,8 +20,8 @@ use tower_http::trace::TraceLayer; use crate::appstate::AppState; use crate::config::AppConfig; use crate::handler::{ - balance as balance_handlers, chain as chain_handlers, - governance as gov_handlers, pos as pos_handlers, + balance as balance_handlers, chain as chain_handlers, gas as gas_handlers, + governance as gov_handlers, pk as pk_handlers, pos as pos_handlers, }; use crate::state::common::CommonState; @@ -37,9 +38,10 @@ impl ApplicationServer { let cache_url = config.cache_url.clone(); let app_state = AppState::new(db_url, cache_url); + let client = HttpClient::new(config.tendermint_url.as_str()).unwrap(); let routes = { - let common_state = CommonState::new(app_state.clone()); + let common_state = CommonState::new(client, app_state.clone()); Router::new() .route("/pos/validator", get(pos_handlers::get_validators)) @@ -74,10 +76,20 @@ impl ApplicationServer { "/gov/proposal/:id/votes/:address", get(gov_handlers::get_governance_proposal_votes_by_address), ) + .route( + "/gov/voter/:address/votes", + get(gov_handlers::get_governance_proposal_votes_by_voter), + ) .route( "/account/:address", get(balance_handlers::get_address_balance), ) + .route( + "/revealed_public_key/:address", + get(pk_handlers::get_revealed_pk), + ) + // TODO: this is a temporary endpoint, we should get gas cost per tx type + .route("/gas_table", get(gas_handlers::get_gas_table)) .route("/chain/sync", get(chain_handlers::sync_height)) .with_state(common_state) }; diff --git a/webserver/src/config.rs b/webserver/src/config.rs index c9ffe4753..9c652c7eb 100644 --- a/webserver/src/config.rs +++ b/webserver/src/config.rs @@ -17,4 +17,7 @@ pub struct AppConfig { #[clap(long, env)] pub rps: Option, + + #[clap(long, env)] + pub tendermint_url: String, } diff --git a/webserver/src/dto/pos.rs b/webserver/src/dto/pos.rs index 5195e9eae..2a54b1843 100644 --- a/webserver/src/dto/pos.rs +++ b/webserver/src/dto/pos.rs @@ -1,10 +1,8 @@ use serde::{Deserialize, Serialize}; use validator::Validate; -use super::utils::Pagination; - #[derive(Clone, Serialize, Deserialize, Validate)] pub struct PoSQueryParams { - #[serde(flatten)] - pub pagination: Option, + #[validate(range(min = 1, max = 10000))] + pub page: Option, } diff --git a/webserver/src/dto/utils.rs b/webserver/src/dto/utils.rs index bb9f802d3..aea26e990 100644 --- a/webserver/src/dto/utils.rs +++ b/webserver/src/dto/utils.rs @@ -1,7 +1,7 @@ use serde::{Deserialize, Serialize}; use validator::Validate; -#[derive(Clone, Serialize, Deserialize, Validate)] +#[derive(Debug, Clone, Serialize, Deserialize, Validate)] pub struct Pagination { #[validate(range(min = 1, max = 10000))] pub page: u64, diff --git a/webserver/src/error/api.rs b/webserver/src/error/api.rs index cde31f6d5..d546a58f3 100644 --- a/webserver/src/error/api.rs +++ b/webserver/src/error/api.rs @@ -5,6 +5,7 @@ use thiserror::Error; use super::balance::BalanceError; use super::governance::GovernanceError; use super::pos::PoSError; +use super::revealed_pk::RevealedPkError; use crate::response::api::ApiErrorResponse; #[derive(Error, Debug)] @@ -15,6 +16,8 @@ pub enum ApiError { BalanceError(#[from] BalanceError), #[error(transparent)] GovernanceError(#[from] GovernanceError), + #[error(transparent)] + RevealedPkError(#[from] RevealedPkError), #[error("No chain parameters stored")] NoChainParameters, #[error("Invalid request header")] @@ -33,6 +36,7 @@ impl IntoResponse for ApiError { ApiError::PoSError(error) => error.into_response(), ApiError::BalanceError(error) => error.into_response(), ApiError::GovernanceError(error) => error.into_response(), + ApiError::RevealedPkError(error) => error.into_response(), ApiError::InvalidHeader => ApiErrorResponse::send( StatusCode::BAD_REQUEST.as_u16(), Some("Invalid Header".to_string()), diff --git a/webserver/src/error/mod.rs b/webserver/src/error/mod.rs index 1e1daa86f..e5828d668 100644 --- a/webserver/src/error/mod.rs +++ b/webserver/src/error/mod.rs @@ -2,3 +2,4 @@ pub mod api; pub mod balance; pub mod governance; pub mod pos; +pub mod revealed_pk; diff --git a/webserver/src/error/revealed_pk.rs b/webserver/src/error/revealed_pk.rs new file mode 100644 index 000000000..8b6255b1a --- /dev/null +++ b/webserver/src/error/revealed_pk.rs @@ -0,0 +1,30 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use thiserror::Error; + +use crate::response::api::ApiErrorResponse; + +#[derive(Error, Debug)] +pub enum RevealedPkError { + #[error("Revealed public key {0} not found")] + NotFound(u64), + #[error("Database error: {0}")] + Database(String), + #[error("Rpc error: {0}")] + Rpc(String), + #[error("Unknown error: {0}")] + Unknown(String), +} + +impl IntoResponse for RevealedPkError { + fn into_response(self) -> Response { + let status_code = match self { + RevealedPkError::NotFound(_) => StatusCode::NOT_FOUND, + RevealedPkError::Unknown(_) + | RevealedPkError::Database(_) + | RevealedPkError::Rpc(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + + ApiErrorResponse::send(status_code.as_u16(), Some(self.to_string())) + } +} diff --git a/webserver/src/handler/gas.rs b/webserver/src/handler/gas.rs new file mode 100644 index 000000000..0bbe9f683 --- /dev/null +++ b/webserver/src/handler/gas.rs @@ -0,0 +1,20 @@ +use axum::extract::State; +use axum::http::HeaderMap; +use axum::Json; +use axum_macros::debug_handler; +use axum_trace_id::TraceId; + +use crate::error::api::ApiError; +use crate::response::gas::GasCost; +use crate::state::common::CommonState; + +#[debug_handler] +pub async fn get_gas_table( + _trace_id: TraceId, + _headers: HeaderMap, + State(state): State, +) -> Result>, ApiError> { + let gas_table = state.gas_service.get_gas_table(&state.client).await; + + Ok(Json(gas_table)) +} diff --git a/webserver/src/handler/governance.rs b/webserver/src/handler/governance.rs index cb5f544fc..d5147b9d0 100644 --- a/webserver/src/handler/governance.rs +++ b/webserver/src/handler/governance.rs @@ -102,3 +102,18 @@ pub async fn get_governance_proposal_votes_by_address( Ok(Json(proposal_votes)) } + +#[debug_handler] +pub async fn get_governance_proposal_votes_by_voter( + _trace_id: TraceId, + _headers: HeaderMap, + Path(address): Path, + State(state): State, +) -> Result>, ApiError> { + let proposal_votes = state + .gov_service + .find_governance_proposal_votes_by_voter(address) + .await?; + + Ok(Json(proposal_votes)) +} diff --git a/webserver/src/handler/mod.rs b/webserver/src/handler/mod.rs index aa95e0d78..70fcc9317 100644 --- a/webserver/src/handler/mod.rs +++ b/webserver/src/handler/mod.rs @@ -1,4 +1,6 @@ pub mod balance; pub mod chain; +pub mod gas; pub mod governance; +pub mod pk; pub mod pos; diff --git a/webserver/src/handler/pk.rs b/webserver/src/handler/pk.rs new file mode 100644 index 000000000..43f5056e9 --- /dev/null +++ b/webserver/src/handler/pk.rs @@ -0,0 +1,24 @@ +use axum::extract::{Path, State}; +use axum::http::HeaderMap; +use axum::Json; +use axum_macros::debug_handler; +use axum_trace_id::TraceId; + +use crate::error::api::ApiError; +use crate::response::revealed_pk::RevealedPk; +use crate::state::common::CommonState; + +#[debug_handler] +pub async fn get_revealed_pk( + _trace_id: TraceId, + _headers: HeaderMap, + Path(address): Path, + State(state): State, +) -> Result, ApiError> { + let revealed_pk = state + .revealed_pk_service + .get_revealed_pk_by_address(&state.client, address) + .await?; + + Ok(Json(revealed_pk)) +} diff --git a/webserver/src/handler/pos.rs b/webserver/src/handler/pos.rs index bc28d7d62..336a6aa3b 100644 --- a/webserver/src/handler/pos.rs +++ b/webserver/src/handler/pos.rs @@ -19,7 +19,7 @@ pub async fn get_validators( Query(query): Query, State(state): State, ) -> Result>>, ApiError> { - let page = query.pagination.map(|p| p.page).unwrap_or(1); + let page = query.page.unwrap_or(1); let (validators, total_validators) = state.pos_service.get_all_validators(page).await?; diff --git a/webserver/src/repository/chain.rs b/webserver/src/repository/chain.rs index c2daedce6..6958af93d 100644 --- a/webserver/src/repository/chain.rs +++ b/webserver/src/repository/chain.rs @@ -15,6 +15,8 @@ pub trait ChainRepositoryTrait { fn new(app_state: AppState) -> Self; async fn find_latest_height(&self) -> Result, String>; + + async fn find_latest_epoch(&self) -> Result, String>; } #[async_trait] @@ -35,4 +37,17 @@ impl ChainRepositoryTrait for ChainRepository { .map_err(|e| e.to_string())? .map_err(|e| e.to_string()) } + + async fn find_latest_epoch(&self) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + block_crawler_state::dsl::block_crawler_state + .select(max(block_crawler_state::dsl::epoch)) + .first::>(conn) + }) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string()) + } } diff --git a/webserver/src/repository/governance.rs b/webserver/src/repository/governance.rs index 405cff904..ee8315cfa 100644 --- a/webserver/src/repository/governance.rs +++ b/webserver/src/repository/governance.rs @@ -49,6 +49,11 @@ pub trait GovernanceRepoTrait { proposal_id: i32, voter_address: String, ) -> Result, String>; + + async fn find_governance_proposal_votes_by_voter( + &self, + voter_address: String, + ) -> Result, String>; } #[async_trait] @@ -153,4 +158,21 @@ impl GovernanceRepoTrait for GovernanceRepo { .map_err(|e| e.to_string())? .map_err(|e| e.to_string()) } + + async fn find_governance_proposal_votes_by_voter( + &self, + voter_address: String, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + governance_votes::table + .filter(governance_votes::dsl::voter_address.eq(voter_address)) + .select(GovernanceProposalVoteDb::as_select()) + .get_results(conn) + }) + .await + .map_err(|e| e.to_string())? + .map_err(|e| e.to_string()) + } } diff --git a/webserver/src/repository/mod.rs b/webserver/src/repository/mod.rs index 5310da139..a5a54be62 100644 --- a/webserver/src/repository/mod.rs +++ b/webserver/src/repository/mod.rs @@ -2,4 +2,5 @@ pub mod balance; pub mod chain; pub mod governance; pub mod pos; +pub mod revealed_pk; pub mod utils; diff --git a/webserver/src/repository/revealed_pk.rs b/webserver/src/repository/revealed_pk.rs new file mode 100644 index 000000000..a03ed5751 --- /dev/null +++ b/webserver/src/repository/revealed_pk.rs @@ -0,0 +1,69 @@ +use axum::async_trait; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; +use orm::revealed_pk::{RevealedPkDb, RevealedPkInsertDb}; +use orm::schema::revealed_pk; + +use crate::appstate::AppState; + +#[derive(Clone)] +pub struct RevealedPkRepo { + pub(crate) app_state: AppState, +} + +#[async_trait] +pub trait PkRepoTrait { + fn new(app_state: AppState) -> Self; + + async fn get_revealed_pk_by_address( + &self, + address: String, + ) -> Result, String>; + + async fn insert_revealed_pk( + &self, + address: RevealedPkInsertDb, + ) -> Result<(), String>; +} + +#[async_trait] +impl PkRepoTrait for RevealedPkRepo { + fn new(app_state: AppState) -> Self { + Self { app_state } + } + + async fn get_revealed_pk_by_address( + &self, + address: String, + ) -> Result, String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| { + revealed_pk::table + .filter(revealed_pk::dsl::address.eq(address)) + .select(RevealedPkDb::as_select()) + .first(conn) + .ok() + }) + .await + .map_err(|e| e.to_string()) + } + + async fn insert_revealed_pk( + &self, + revealed_pk: RevealedPkInsertDb, + ) -> Result<(), String> { + let conn = self.app_state.get_db_connection().await; + + conn.interact(move |conn| -> Result<(), String> { + diesel::insert_into(revealed_pk::table) + .values::>(vec![revealed_pk]) + .on_conflict_do_nothing() + .execute(conn) + .map_err(|e| e.to_string())?; + + Ok(()) + }) + .await + .map_err(|e| e.to_string())? + } +} diff --git a/webserver/src/response/balance.rs b/webserver/src/response/balance.rs index 72fdc3ccd..e534be83f 100644 --- a/webserver/src/response/balance.rs +++ b/webserver/src/response/balance.rs @@ -5,14 +5,14 @@ use serde::{Deserialize, Serialize}; #[serde(rename_all = "camelCase")] pub struct AddressBalance { pub token_address: String, - pub balances: String, + pub balance: String, } impl From for AddressBalance { fn from(value: BalanceDb) -> Self { Self { token_address: value.token, - balances: value.raw_amount, + balance: value.raw_amount, } } } diff --git a/webserver/src/response/gas.rs b/webserver/src/response/gas.rs new file mode 100644 index 000000000..5bdd0b580 --- /dev/null +++ b/webserver/src/response/gas.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct GasCost { + pub token_address: String, + pub amount: String, +} diff --git a/webserver/src/response/governance.rs b/webserver/src/response/governance.rs index 2922aa9a8..95636dbca 100644 --- a/webserver/src/response/governance.rs +++ b/webserver/src/response/governance.rs @@ -1,7 +1,9 @@ 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}; @@ -15,6 +17,39 @@ pub enum ProposalType { PgfFunding, } +impl Display for ProposalType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ProposalType::Default => write!(f, "default"), + ProposalType::DefaultWithWasm => write!(f, "default_with_wasm"), + ProposalType::PgfSteward => write!(f, "pgf_steward"), + ProposalType::PgfFunding => write!(f, "pgf_funding"), + } + } +} + +#[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 { @@ -51,15 +86,19 @@ 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, pub end_epoch: u64, pub activation_epoch: u64, + pub start_time: i64, + pub end_time: i64, + pub current_time: i64, pub status: ProposalStatus, - pub yay_votes: u64, - pub nay_votes: u64, - pub abstrain_votes: u64, + pub yay_votes: String, + pub nay_votes: String, + pub abstain_votes: String, } #[derive(Clone, Debug, Deserialize, Serialize)] @@ -70,8 +109,26 @@ pub struct ProposalVote { pub voter_address: String, } -impl From for Proposal { - fn from(value: GovernanceProposalDb) -> Self { +// TODO: read it from storage later +const CONSENSUS_TIME_IN_SEC: i64 = 10; +const MIN_NUMBER_OF_BLOCKS: i64 = 4; + +impl Proposal { + pub fn from_proposal_db( + value: GovernanceProposalDb, + current_epoch: i32, + current_block: i32, + ) -> Self { + let seconds_since_beginning = + i64::from(current_block) * CONSENSUS_TIME_IN_SEC; + let seconds_until_end = i64::from(value.end_epoch - current_epoch) + * MIN_NUMBER_OF_BLOCKS + * CONSENSUS_TIME_IN_SEC; + + let time_now = DateTimeUtc::now().0.timestamp(); + let start_time = time_now - seconds_since_beginning; + let end_time = time_now + seconds_until_end; + Self { id: value.id as u64, content: value.content, @@ -87,11 +144,27 @@ impl From for 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, end_epoch: value.end_epoch as u64, activation_epoch: value.activation_epoch as u64, + + start_time, + end_time, + current_time: time_now, + status: match value.result { GovernanceProposalResultDb::Passed => ProposalStatus::Passed, GovernanceProposalResultDb::Rejected => { @@ -103,12 +176,9 @@ impl From for Proposal { ProposalStatus::Voting } }, - yay_votes: value.yay_votes.parse::().unwrap_or_default(), - nay_votes: value.nay_votes.parse::().unwrap_or_default(), - abstrain_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/response/mod.rs b/webserver/src/response/mod.rs index 381e887e3..8174dff9f 100644 --- a/webserver/src/response/mod.rs +++ b/webserver/src/response/mod.rs @@ -1,5 +1,7 @@ pub mod api; pub mod balance; +pub mod gas; pub mod governance; pub mod pos; +pub mod revealed_pk; pub mod utils; diff --git a/webserver/src/response/pos.rs b/webserver/src/response/pos.rs index cfca55f78..43e894aa4 100644 --- a/webserver/src/response/pos.rs +++ b/webserver/src/response/pos.rs @@ -11,6 +11,7 @@ pub struct Validator { pub voting_power: String, pub max_commission: String, pub commission: String, + pub name: Option, pub email: Option, pub website: Option, pub description: Option, @@ -61,6 +62,7 @@ impl From for Validator { voting_power: value.voting_power.to_string(), max_commission: value.max_commission, commission: value.commission, + name: value.name, email: value.email, website: value.website, description: value.description, diff --git a/webserver/src/response/revealed_pk.rs b/webserver/src/response/revealed_pk.rs new file mode 100644 index 000000000..0c40d0aa9 --- /dev/null +++ b/webserver/src/response/revealed_pk.rs @@ -0,0 +1,16 @@ +use orm::revealed_pk::RevealedPkDb; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct RevealedPk { + pub public_key: Option, +} + +impl From for RevealedPk { + fn from(value: RevealedPkDb) -> Self { + Self { + public_key: Some(value.pk), + } + } +} diff --git a/webserver/src/service/balance.rs b/webserver/src/service/balance.rs index 8c61d48ab..7f2f519b6 100644 --- a/webserver/src/service/balance.rs +++ b/webserver/src/service/balance.rs @@ -3,6 +3,8 @@ 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, @@ -25,6 +27,16 @@ impl BalanceService { .await .map_err(BalanceError::Database)?; - Ok(balances.into_iter().map(AddressBalance::from).collect()) + // TODO: temporary solution as we only store NAM balances + let denominated_balances: Vec = balances + .iter() + .cloned() + .map(|balance| AddressBalance { + token_address: balance.token, + balance: raw_amount_to_nam(balance.raw_amount), + }) + .collect(); + + Ok(denominated_balances) } } diff --git a/webserver/src/service/gas.rs b/webserver/src/service/gas.rs new file mode 100644 index 000000000..b601d6ef6 --- /dev/null +++ b/webserver/src/service/gas.rs @@ -0,0 +1,39 @@ +use std::collections::BTreeMap; + +use namada_parameters::storage as parameter_storage; +use namada_sdk::{ + address::Address, rpc::query_storage_value, tendermint_rpc::HttpClient, + token, +}; + +use crate::{appstate::AppState, response::gas::GasCost}; + +#[derive(Clone)] +pub struct GasService {} + +impl GasService { + pub fn new(_app_state: AppState) -> Self { + Self {} + } + + pub async fn get_gas_table(&self, client: &HttpClient) -> Vec { + let key = parameter_storage::get_gas_cost_key(); + let gas_cost_table = query_storage_value::< + HttpClient, + BTreeMap, + >(client, &key) + .await + .expect("Parameter should be defined."); + + let mut gas_table: Vec = Vec::new(); + + for (token, gas_cost) in gas_cost_table { + gas_table.push(GasCost { + token_address: token.to_string(), + amount: gas_cost.to_string_native(), + }) + } + + gas_table + } +} diff --git a/webserver/src/service/governance.rs b/webserver/src/service/governance.rs index 0b06406af..bc35b72d7 100644 --- a/webserver/src/service/governance.rs +++ b/webserver/src/service/governance.rs @@ -3,18 +3,21 @@ use orm::governance_proposal::GovernanceProposalResultDb; use crate::appstate::AppState; use crate::dto::governance::ProposalStatus; use crate::error::governance::GovernanceError; +use crate::repository::chain::{ChainRepository, ChainRepositoryTrait}; use crate::repository::governance::{GovernanceRepo, GovernanceRepoTrait}; use crate::response::governance::{Proposal, ProposalVote}; #[derive(Clone)] pub struct GovernanceService { governance_repo: GovernanceRepo, + chain_repo: ChainRepository, } impl GovernanceService { pub fn new(app_state: AppState) -> Self { Self { - governance_repo: GovernanceRepo::new(app_state), + governance_repo: GovernanceRepo::new(app_state.clone()), + chain_repo: ChainRepository::new(app_state), } } @@ -51,8 +54,27 @@ impl GovernanceService { .await .map_err(GovernanceError::Database)?; + let latest_epoch = self + .chain_repo + .find_latest_epoch() + .await + .map_err(GovernanceError::Database)? + .expect("latest epoch not found"); + + let latest_block = self + .chain_repo + .find_latest_height() + .await + .map_err(GovernanceError::Database)? + .expect("latest block not found"); + Ok(( - db_proposals.into_iter().map(Proposal::from).collect(), + db_proposals + .into_iter() + .map(|p| { + Proposal::from_proposal_db(p, latest_epoch, latest_block) + }) + .collect(), total_items as u64, )) } @@ -67,7 +89,22 @@ impl GovernanceService { .await .map_err(GovernanceError::Database)?; - Ok(db_proposal.map(Proposal::from)) + let latest_epoch = self + .chain_repo + .find_latest_epoch() + .await + .map_err(GovernanceError::Database)? + .expect("latest epoch not found"); + + let latest_block = self + .chain_repo + .find_latest_height() + .await + .map_err(GovernanceError::Database)? + .expect("latest block not found"); + + Ok(db_proposal + .map(|p| Proposal::from_proposal_db(p, latest_epoch, latest_block))) } pub async fn search_governance_proposals_by_pattern( @@ -85,8 +122,27 @@ impl GovernanceService { .await .map_err(GovernanceError::Database)?; + let latest_epoch = self + .chain_repo + .find_latest_epoch() + .await + .map_err(GovernanceError::Database)? + .expect("latest epoch not found"); + + let latest_block = self + .chain_repo + .find_latest_height() + .await + .map_err(GovernanceError::Database)? + .expect("latest block not found"); + Ok(( - db_proposals.into_iter().map(Proposal::from).collect(), + db_proposals + .into_iter() + .map(|p| { + Proposal::from_proposal_db(p, latest_epoch, latest_block) + }) + .collect(), total_items as u64, )) } @@ -150,4 +206,20 @@ impl GovernanceService { .map(ProposalVote::from) .collect()) } + + pub async fn find_governance_proposal_votes_by_voter( + &self, + voter_address: String, + ) -> Result, GovernanceError> { + let db_proposal_votes = self + .governance_repo + .find_governance_proposal_votes_by_voter(voter_address) + .await + .map_err(GovernanceError::Database)?; + + Ok(db_proposal_votes + .into_iter() + .map(ProposalVote::from) + .collect()) + } } diff --git a/webserver/src/service/mod.rs b/webserver/src/service/mod.rs index aa95e0d78..36c092c5e 100644 --- a/webserver/src/service/mod.rs +++ b/webserver/src/service/mod.rs @@ -1,4 +1,7 @@ pub mod balance; pub mod chain; +pub mod gas; pub mod governance; pub mod pos; +pub mod revealed_pk; +pub mod utils; diff --git a/webserver/src/service/pos.rs b/webserver/src/service/pos.rs index 5142648b5..59af37ae0 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, @@ -59,7 +61,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( @@ -87,7 +98,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( @@ -101,14 +120,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", @@ -116,7 +135,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( @@ -144,9 +171,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/revealed_pk.rs b/webserver/src/service/revealed_pk.rs new file mode 100644 index 000000000..67ef80176 --- /dev/null +++ b/webserver/src/service/revealed_pk.rs @@ -0,0 +1,70 @@ +use std::str::FromStr; + +use crate::appstate::AppState; +use crate::error::revealed_pk::RevealedPkError; +use crate::repository::revealed_pk::{PkRepoTrait, RevealedPkRepo}; +use crate::response::revealed_pk::RevealedPk; + +use namada_sdk::address::Address as NamadaAddress; +use namada_sdk::rpc; +use namada_sdk::tendermint_rpc::HttpClient; +use orm::revealed_pk::RevealedPkInsertDb; + +#[derive(Clone)] +pub struct RevealedPkService { + pub revealed_pk_repo: RevealedPkRepo, +} + +impl RevealedPkService { + pub fn new(app_state: AppState) -> Self { + Self { + revealed_pk_repo: RevealedPkRepo::new(app_state), + } + } + + pub async fn get_revealed_pk_by_address( + &self, + client: &HttpClient, + address: String, + ) -> Result { + // We look for a revealed public key in the database + let revealed_pk_db = self + .revealed_pk_repo + .get_revealed_pk_by_address(address.clone()) + .await + .map_err(RevealedPkError::Database)?; + + match revealed_pk_db { + // If we find a revealed public key in the database, we return it + Some(revealed_pk_db) => Ok(RevealedPk::from(revealed_pk_db)), + // If we don't find a revealed public key in the database, we look for it in the + // storage + None => { + let address = + NamadaAddress::from_str(&address).expect("Invalid address"); + + let public_key = rpc::get_public_key_at(client, &address, 0) + .await + .map_err(|e| RevealedPkError::Rpc(e.to_string()))?; + + // If we find a public key in the storage, we insert it in the database + if let Some(public_key) = public_key.clone() { + // TODO: maybe better to create it using strcuts from shared + let revealed_pk_db = RevealedPkInsertDb { + pk: public_key.to_string(), + address: address.to_string(), + }; + + self.revealed_pk_repo + .insert_revealed_pk(revealed_pk_db) + .await + .map_err(RevealedPkError::Database)?; + }; + + Ok(RevealedPk { + public_key: public_key.map(|pk| pk.to_string()), + }) + } + } + } +} 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() +} diff --git a/webserver/src/state/common.rs b/webserver/src/state/common.rs index 27844d3f1..9f58cba21 100644 --- a/webserver/src/state/common.rs +++ b/webserver/src/state/common.rs @@ -1,8 +1,12 @@ +use namada_sdk::tendermint_rpc::HttpClient; + use crate::appstate::AppState; use crate::service::balance::BalanceService; use crate::service::chain::ChainService; +use crate::service::gas::GasService; use crate::service::governance::GovernanceService; use crate::service::pos::PosService; +use crate::service::revealed_pk::RevealedPkService; #[derive(Clone)] pub struct CommonState { @@ -10,15 +14,21 @@ pub struct CommonState { pub gov_service: GovernanceService, pub balance_service: BalanceService, pub chain_service: ChainService, + pub revealed_pk_service: RevealedPkService, + pub gas_service: GasService, + pub client: HttpClient, } impl CommonState { - pub fn new(data: AppState) -> Self { + pub fn new(client: HttpClient, data: AppState) -> Self { Self { pos_service: PosService::new(data.clone()), gov_service: GovernanceService::new(data.clone()), balance_service: BalanceService::new(data.clone()), chain_service: ChainService::new(data.clone()), + revealed_pk_service: RevealedPkService::new(data.clone()), + gas_service: GasService::new(data.clone()), + client, } } }