From fa2faa4a576c59d0155a4956e41ac94f9b930674 Mon Sep 17 00:00:00 2001 From: Alex Pozhylenkov Date: Fri, 30 Sep 2022 11:18:10 +0300 Subject: [PATCH] [NPG-3427, NPG-3367] Snapshot endpoints: "/api/v0/snapshot/voter/{tag}/{voting_key}:", " /api/v0/snapshot/delegator/{tag}/{stake_public_key}:" (#280) * update snapshot endpoints * update * add new get_delegator_info endpoint * add voting_power_saturation field to the VoterInfo * fix, update test * update test * update docs * fix * fix clippy * update --- doc/api/v0.yaml | 118 +++--- .../2020-05-22-112032_setup_db/down.sql | 2 +- .../2020-05-22-112032_setup_db/up.sql | 2 +- .../src/db/models/snapshot.rs | 93 ++--- .../src/db/queries/snapshot.rs | 64 ++- vit-servicing-station-lib/src/db/schema.rs | 2 +- .../src/v0/endpoints/snapshot/handlers.rs | 57 +-- .../src/v0/endpoints/snapshot/mod.rs | 370 ++++++++++++++---- .../src/v0/endpoints/snapshot/routes.rs | 13 +- .../src/common/clients/rest/path.rs | 2 +- .../src/common/snapshot.rs | 14 +- 11 files changed, 489 insertions(+), 248 deletions(-) diff --git a/doc/api/v0.yaml b/doc/api/v0.yaml index 80aff397..5ee0826e 100644 --- a/doc/api/v0.yaml +++ b/doc/api/v0.yaml @@ -10,8 +10,6 @@ info: tags: - name: fund description: Information on treasury fund campaigns. - - name: user - description: Information about the user - name: challenge description: Information on challenges, structuring proposals within a fund. - name: proposal @@ -92,32 +90,6 @@ paths: "404": description: The requested fund was not found - /api/v0/user/{id}: - description: Manages Cardano user information by the provided user id - get: - operationId: getUserById - tags: - - user - description: Retrieves user info - summary: get user info by id - parameters: - - name: id - in: path - required: true - deprecated: false - schema: - type: string - description: user's id - responses: - "200": - description: user's info - content: - application/json: - schema: - $ref: '#/components/schemas/UserInfo' - "404": - description: The requested user was not found - /api/v0/proposals: post: summary: Get proposals by chain id @@ -303,13 +275,13 @@ paths: "400": description: Invalid combination of table/column (e.g. using funds column on challenges table) - /api/v0/snapshot/{tag}/{voting_key}: + /api/v0/snapshot/voter/{tag}/{voting_key}: get: - operationId: getVotingPower - summary: Get voting power by voting key + operationId: getVoterInfo + summary: Get voter's info by voting key tags: [snapshot] description: | - Get voting power by voting key + Get voter's info by voting key parameters: - in: path name: tag @@ -327,7 +299,35 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/VotingPower" + $ref: "#/components/schemas/VotersInfo" + "400": + description: Not found + + /api/v0/snapshot/delegator/{tag}/{stake_public_key}: + get: + operationId: getDelegatorInfo + summary: Get delegator's info by stake public key + tags: [snapshot] + description: | + Get delegator's info by stake public key + parameters: + - in: path + name: tag + schema: + type: string + required: true + - in: path + name: stake_public_key + schema: + type: string + required: true + responses: + "200": + description: Success + content: + application/json: + schema: + $ref: "#/components/schemas/VotersInfo" "400": description: Not found @@ -958,38 +958,55 @@ components: type: string enum: [title, type, desc, author, funds] - VotingPower: + VotersInfo: properties: - voting_info: + voter_info: type: array items: - $ref: "#/components/schemas/VotingInfo" - last_update: + $ref: "#/components/schemas/VoterInfo" + last_updated: type: string format: date-time + description: Date and time for the latest update to this snapshot information. - VotingInfo: + VoterInfo: + type: object + description: voter's info + required: + - voting_power + - voting_group + - delegations_power + - delegations_count + - voting_power_saturation properties: voting_power: + description: voter's voting power type: integer format: u64 voting_group: + description: voter's voting group on which he is assigned type: string delegations_power: + description: voter's delegation's power, which represents total voting power which was delegated to this voter type: integer format: u64 delegations_count: + description: amount of delegators, who was delegated to this voter type: integer format: u64 + voting_power_saturation: + description: voting power's share of the total voting power corresponds to the current voting group + type: number + minimum: 0 + maximum: 1 example: - [ { "voting_power": 1000, "voting_group": "representative", "delegations_power": 400, "delegations_count": 200, - }, - ] + "voting_power_saturation": 0.5, + } NextFundInfo: properties: @@ -1093,17 +1110,15 @@ components: type: string format: date-time description: Date and time for the latest update to this snapshot information. - UserInfo: + + DelegatorInfo: type: object - description: User info + description: delegator's info required: - - is_voter - delegators - voting_groups + - last_updated properties: - is_voter: - type: boolean - description: Is this user an active voter (representative) or delegator dreps: type: array description: List of delegated representatives for the current user @@ -1117,11 +1132,14 @@ components: items: description: Voting group id (the same as voting token id) type: string - pattern: '[0-9a-f]{56}.[0-9a-f]+' + last_updated: + description: Date and time for the latest update to this snapshot information + type: string + format: date-time example: { - is_voter: true, delegators: ["f5285eeead8b5885a1420800de14b0d1960db1a990a6c2f7b517125bedc000db", "7ef044ba437057d6d944ace679b7f811335639a689064cd969dffc8b55a7cc19"], - voting_groups: ["00000000000000000000000000000000000000000000000000000000.6c1e8abc"] + voting_groups: ["group1", "group2"], + last_updated: "2021-02-11T10:10:27+00:00" } diff --git a/vit-servicing-station-lib/migrations/2020-05-22-112032_setup_db/down.sql b/vit-servicing-station-lib/migrations/2020-05-22-112032_setup_db/down.sql index 20382f45..cb8802ac 100644 --- a/vit-servicing-station-lib/migrations/2020-05-22-112032_setup_db/down.sql +++ b/vit-servicing-station-lib/migrations/2020-05-22-112032_setup_db/down.sql @@ -13,4 +13,4 @@ DROP TABLE IF EXISTS groups; DROP TABLE IF EXISTS votes; DROP TABLE IF EXISTS snapshots; DROP TABLE IF EXISTS voters; -DROP TABLE IF EXISTS contributors; +DROP TABLE IF EXISTS contributions; diff --git a/vit-servicing-station-lib/migrations/2020-05-22-112032_setup_db/up.sql b/vit-servicing-station-lib/migrations/2020-05-22-112032_setup_db/up.sql index 386388bd..4a0e8584 100644 --- a/vit-servicing-station-lib/migrations/2020-05-22-112032_setup_db/up.sql +++ b/vit-servicing-station-lib/migrations/2020-05-22-112032_setup_db/up.sql @@ -159,7 +159,7 @@ create table voters ( FOREIGN KEY(snapshot_tag) REFERENCES snapshots(tag) ON DELETE CASCADE ); -create table contributors ( +create table contributions ( stake_public_key TEXT NOT NULL, reward_address TEXT NOT NULL, value BIGINT NOT NULL, diff --git a/vit-servicing-station-lib/src/db/models/snapshot.rs b/vit-servicing-station-lib/src/db/models/snapshot.rs index 176595ce..66dc0470 100644 --- a/vit-servicing-station-lib/src/db/models/snapshot.rs +++ b/vit-servicing-station-lib/src/db/models/snapshot.rs @@ -1,8 +1,9 @@ -use crate::db::schema::{contributors, snapshots, voters}; -use diesel::{ExpressionMethods, Insertable, Queryable}; +use crate::db::schema::{contributions, snapshots, voters}; +use diesel::{Insertable, Queryable}; use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Queryable)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Queryable, Insertable)] +#[diesel(table_name = "snapshots")] #[serde(rename_all = "camelCase")] pub struct Snapshot { /// Tag - a unique identifier of the current snapshot @@ -13,21 +14,22 @@ pub struct Snapshot { pub last_updated: i64, } -impl Insertable for Snapshot { - type Values = ( - diesel::dsl::Eq, - diesel::dsl::Eq, - ); +// impl Insertable for Snapshot { +// type Values = ( +// diesel::dsl::Eq, +// diesel::dsl::Eq, +// ); - fn values(self) -> Self::Values { - ( - snapshots::tag.eq(self.tag), - snapshots::last_updated.eq(self.last_updated), - ) - } -} +// fn values(self) -> Self::Values { +// ( +// snapshots::tag.eq(self.tag), +// snapshots::last_updated.eq(self.last_updated), +// ) +// } +// } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Queryable)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Queryable, Insertable)] +#[diesel(table_name = "voters")] #[serde(rename_all = "camelCase")] pub struct Voter { pub voting_key: String, @@ -36,27 +38,28 @@ pub struct Voter { pub snapshot_tag: String, } -impl Insertable for Voter { - type Values = ( - diesel::dsl::Eq, - diesel::dsl::Eq, - diesel::dsl::Eq, - diesel::dsl::Eq, - ); +// impl Insertable for Voter { +// type Values = ( +// diesel::dsl::Eq, +// diesel::dsl::Eq, +// diesel::dsl::Eq, +// diesel::dsl::Eq, +// ); - fn values(self) -> Self::Values { - ( - voters::voting_key.eq(self.voting_key), - voters::voting_power.eq(self.voting_power), - voters::voting_group.eq(self.voting_group), - voters::snapshot_tag.eq(self.snapshot_tag), - ) - } -} +// fn values(self) -> Self::Values { +// ( +// voters::voting_key.eq(self.voting_key), +// voters::voting_power.eq(self.voting_power), +// voters::voting_group.eq(self.voting_group), +// voters::snapshot_tag.eq(self.snapshot_tag), +// ) +// } +// } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Queryable)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Queryable, Insertable)] +#[diesel(table_name = "contributions")] #[serde(rename_all = "camelCase")] -pub struct Contributor { +pub struct Contribution { pub stake_public_key: String, pub reward_address: String, pub value: i64, @@ -64,25 +67,3 @@ pub struct Contributor { pub voting_group: String, pub snapshot_tag: String, } - -impl Insertable for Contributor { - type Values = ( - diesel::dsl::Eq, - diesel::dsl::Eq, - diesel::dsl::Eq, - diesel::dsl::Eq, - diesel::dsl::Eq, - diesel::dsl::Eq, - ); - - fn values(self) -> Self::Values { - ( - contributors::stake_public_key.eq(self.stake_public_key), - contributors::reward_address.eq(self.reward_address), - contributors::value.eq(self.value), - contributors::voting_key.eq(self.voting_key), - contributors::voting_group.eq(self.voting_group), - contributors::snapshot_tag.eq(self.snapshot_tag), - ) - } -} diff --git a/vit-servicing-station-lib/src/db/queries/snapshot.rs b/vit-servicing-station-lib/src/db/queries/snapshot.rs index 1f0d5a6e..2edc4061 100644 --- a/vit-servicing-station-lib/src/db/queries/snapshot.rs +++ b/vit-servicing-station-lib/src/db/queries/snapshot.rs @@ -1,12 +1,12 @@ use crate::{ db::{ - models::snapshot::{Contributor, Snapshot, Voter}, - schema::{contributors, snapshots, voters}, + models::snapshot::{Contribution, Snapshot, Voter}, + schema::{contributions, snapshots, voters}, DbConnection, DbConnectionPool, }, v0::errors::HandleError, }; -use diesel::{ExpressionMethods, Insertable, QueryDsl, RunQueryDsl}; +use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl}; pub async fn query_all_snapshots(pool: &DbConnectionPool) -> Result, HandleError> { let db_conn = pool.get().map_err(HandleError::DatabaseError)?; @@ -38,7 +38,7 @@ pub async fn query_snapshot_by_tag( pub fn put_snapshot(snapshot: Snapshot, pool: &DbConnectionPool) -> Result<(), HandleError> { let db_conn = pool.get().map_err(HandleError::DatabaseError)?; diesel::replace_into(snapshots::table) - .values(snapshot.values()) + .values(snapshot) .execute(&db_conn) .map_err(|e| HandleError::InternalError(format!("Error executing request: {}", e)))?; Ok(()) @@ -61,10 +61,25 @@ pub async fn query_voters_by_voting_key_and_snapshot_tag( .map_err(|e| HandleError::InternalError(format!("Error executing voters: {}", e)))? } -pub fn batch_put_voters( - voters: &[>::Values], - db_conn: &DbConnection, -) -> Result<(), HandleError> { +pub async fn query_total_voting_power_by_voting_group_and_snapshot_tag( + voting_group: String, + tag: String, + pool: &DbConnectionPool, +) -> Result { + let db_conn = pool.get().map_err(HandleError::DatabaseError)?; + tokio::task::spawn_blocking(move || { + voters::dsl::voters + .filter(voters::dsl::voting_group.eq(voting_group)) + .filter(voters::dsl::snapshot_tag.eq(tag)) + .load::(&db_conn) + .map_err(|e| HandleError::NotFound(format!("Error loading voters: {}", e))) + .map(|voters| voters.iter().map(|voter| voter.voting_power).sum()) + }) + .await + .map_err(|e| HandleError::InternalError(format!("Error executing voters: {}", e)))? +} + +pub fn batch_put_voters(voters: &[Voter], db_conn: &DbConnection) -> Result<(), HandleError> { diesel::replace_into(voters::table) .values(voters) .execute(db_conn) @@ -72,18 +87,35 @@ pub fn batch_put_voters( Ok(()) } -pub async fn query_contributors_by_voting_key_and_voter_group_and_snapshot_tag( +pub async fn query_contributions_by_voting_key_and_voter_group_and_snapshot_tag( voting_key: String, voting_group: String, tag: String, pool: &DbConnectionPool, -) -> Result, HandleError> { +) -> Result, HandleError> { + let db_conn = pool.get().map_err(HandleError::DatabaseError)?; + tokio::task::spawn_blocking(move || { + contributions::dsl::contributions + .filter(contributions::dsl::voting_key.eq(voting_key)) + .filter(contributions::dsl::voting_group.eq(voting_group)) + .filter(contributions::dsl::snapshot_tag.eq(tag)) + .load(&db_conn) + .map_err(|e| HandleError::NotFound(format!("Error loading contributions: {}", e))) + }) + .await + .map_err(|e| HandleError::InternalError(format!("Error executing request: {}", e)))? +} + +pub async fn query_contributions_by_stake_public_key_and_snapshot_tag( + stake_public_key: String, + tag: String, + pool: &DbConnectionPool, +) -> Result, HandleError> { let db_conn = pool.get().map_err(HandleError::DatabaseError)?; tokio::task::spawn_blocking(move || { - contributors::dsl::contributors - .filter(contributors::dsl::voting_key.eq(voting_key)) - .filter(contributors::dsl::voting_group.eq(voting_group)) - .filter(contributors::dsl::snapshot_tag.eq(tag)) + contributions::dsl::contributions + .filter(contributions::dsl::stake_public_key.eq(stake_public_key)) + .filter(contributions::dsl::snapshot_tag.eq(tag)) .load(&db_conn) .map_err(|e| HandleError::NotFound(format!("Error loading contributions: {}", e))) }) @@ -92,10 +124,10 @@ pub async fn query_contributors_by_voting_key_and_voter_group_and_snapshot_tag( } pub fn batch_put_contributions( - contributions: &[>::Values], + contributions: &[Contribution], db_conn: &DbConnection, ) -> Result<(), HandleError> { - diesel::replace_into(contributors::table) + diesel::replace_into(contributions::table) .values(contributions) .execute(db_conn) .map_err(|e| HandleError::InternalError(format!("Error executing request: {}", e)))?; diff --git a/vit-servicing-station-lib/src/db/schema.rs b/vit-servicing-station-lib/src/db/schema.rs index 1e91a1f7..df743412 100644 --- a/vit-servicing-station-lib/src/db/schema.rs +++ b/vit-servicing-station-lib/src/db/schema.rs @@ -54,7 +54,7 @@ table! { } table! { - contributors (stake_public_key, voting_key, voting_group, snapshot_tag) { + contributions (stake_public_key, voting_key, voting_group, snapshot_tag) { stake_public_key -> Text, reward_address -> Text, value -> BigInt, diff --git a/vit-servicing-station-lib/src/v0/endpoints/snapshot/handlers.rs b/vit-servicing-station-lib/src/v0/endpoints/snapshot/handlers.rs index bd30647f..1a8e3ec5 100644 --- a/vit-servicing-station-lib/src/v0/endpoints/snapshot/handlers.rs +++ b/vit-servicing-station-lib/src/v0/endpoints/snapshot/handlers.rs @@ -1,13 +1,8 @@ -use super::VoterInfo; use crate::v0::context::SharedContext; use crate::v0::result::HandlerResult; -use jormungandr_lib::crypto::account::Identifier; use jormungandr_lib::interfaces::Value; use serde::{Deserialize, Serialize}; -use serde_json::json; use snapshot_lib::{Fraction, RawSnapshot, SnapshotInfo}; -use time::OffsetDateTime; -use warp::http::StatusCode; use warp::{Rejection, Reply}; #[tracing::instrument(skip(context))] @@ -16,34 +11,20 @@ pub async fn get_voters_info( voting_key: String, context: SharedContext, ) -> Result { - let key = if let Ok(key) = Identifier::from_hex(&voting_key) { - key - } else { - return Ok(warp::reply::with_status( - "Invalid voting key", - StatusCode::UNPROCESSABLE_ENTITY, - ) - .into_response()); - }; + Ok(HandlerResult( + super::get_voters_info(tag, voting_key, context).await, + )) +} - match super::get_voters_info(&tag, &key, context).await { - Ok(snapshot) => { - let voter_info: Vec<_> = snapshot.voter_info.into_iter().map(|VoterInfo{voting_group, voting_power,delegations_power, delegations_count}| { - json!({"voting_power": voting_power, "voting_group": voting_group, "delegations_power": delegations_power, "delegations_count": delegations_count}) - }).collect(); - if let Ok(last_update) = OffsetDateTime::from_unix_timestamp(snapshot.last_updated) { - let results = - json!({"voter_info": voter_info, "last_updated": last_update.unix_timestamp()}); - Ok(warp::reply::json(&results).into_response()) - } else { - Ok( - warp::reply::with_status("Invalid time", StatusCode::UNPROCESSABLE_ENTITY) - .into_response(), - ) - } - } - Err(err) => Ok(err.into_response()), - } +#[tracing::instrument(skip(context))] +pub async fn get_delegator_info( + tag: String, + stake_public_key: String, + context: SharedContext, +) -> Result { + Ok(HandlerResult( + super::get_delegator_info(tag, stake_public_key, context).await, + )) } #[tracing::instrument(skip(context))] @@ -55,14 +36,18 @@ pub async fn get_tags(context: SharedContext) -> Result { #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct SnapshotInfoInput { pub snapshot: Vec, - pub update_timestamp: u64, + #[serde(deserialize_with = "crate::utils::serde::deserialize_unix_timestamp_from_rfc3339")] + #[serde(serialize_with = "crate::utils::serde::serialize_unix_timestamp_as_rfc3339")] + pub update_timestamp: i64, } /// Raw Snapshot information update with timestamp. #[derive(Debug, Serialize, Deserialize)] pub struct RawSnapshotInput { pub snapshot: RawSnapshot, - pub update_timestamp: u64, + #[serde(deserialize_with = "crate::utils::serde::deserialize_unix_timestamp_from_rfc3339")] + #[serde(serialize_with = "crate::utils::serde::serialize_unix_timestamp_as_rfc3339")] + pub update_timestamp: i64, pub min_stake_threshold: Value, pub voting_power_cap: Fraction, pub direct_voters_group: Option, @@ -77,7 +62,7 @@ pub async fn put_raw_snapshot( ) -> Result { Ok(HandlerResult( super::update_from_raw_snapshot( - &tag, + tag, input.snapshot, input.update_timestamp, input.min_stake_threshold, @@ -97,7 +82,7 @@ pub async fn put_snapshot_info( context: SharedContext, ) -> Result { Ok(HandlerResult( - super::update_from_shanpshot_info(&tag, input.snapshot, input.update_timestamp, context) + super::update_from_shanpshot_info(tag, input.snapshot, input.update_timestamp, context) .await, )) } diff --git a/vit-servicing-station-lib/src/v0/endpoints/snapshot/mod.rs b/vit-servicing-station-lib/src/v0/endpoints/snapshot/mod.rs index bc79ce69..51f86ac3 100644 --- a/vit-servicing-station-lib/src/v0/endpoints/snapshot/mod.rs +++ b/vit-servicing-station-lib/src/v0/endpoints/snapshot/mod.rs @@ -5,19 +5,21 @@ use crate::{ db::{ models::{ self, - snapshot::{Contributor, Voter}, + snapshot::{Contribution, Voter}, }, queries::snapshot::{ batch_put_contributions, batch_put_voters, put_snapshot, query_all_snapshots, - query_contributors_by_voting_key_and_voter_group_and_snapshot_tag, - query_snapshot_by_tag, query_voters_by_voting_key_and_snapshot_tag, + query_contributions_by_stake_public_key_and_snapshot_tag, + query_contributions_by_voting_key_and_voter_group_and_snapshot_tag, + query_snapshot_by_tag, query_total_voting_power_by_voting_group_and_snapshot_tag, + query_voters_by_voting_key_and_snapshot_tag, }, }, - v0::{context::SharedContext as SharedContext_, errors::HandleError}, + v0::{context::SharedContext, errors::HandleError}, }; -use diesel::Insertable; pub use handlers::{RawSnapshotInput, SnapshotInfoInput}; -use jormungandr_lib::{crypto::account::Identifier, interfaces::Value}; +use itertools::Itertools; +use jormungandr_lib::interfaces::Value; pub use routes::{filter, update_filter}; use serde::{Deserialize, Serialize}; use snapshot_lib::{ @@ -28,43 +30,56 @@ use snapshot_lib::{ pub type Tag = String; pub type Group = String; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct VoterInfo { pub voting_group: Group, pub voting_power: Value, pub delegations_power: u64, pub delegations_count: u64, + pub voting_power_saturation: f64, } /// Voter information in the current snapshot -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct VotersInfo { /// A listing of voter information in the current snapshot pub voter_info: Vec, /// Timestamp for the latest update in voter info in the current snapshot + #[serde(deserialize_with = "crate::utils::serde::deserialize_unix_timestamp_from_rfc3339")] + #[serde(serialize_with = "crate::utils::serde::serialize_unix_timestamp_as_rfc3339")] pub last_updated: i64, } #[tracing::instrument(skip(context))] pub async fn get_voters_info( - tag: &str, - id: &Identifier, - context: SharedContext_, + tag: String, + voting_key: String, + context: SharedContext, ) -> Result { let pool = &context.read().await.db_connection_pool; - let snapshot = query_snapshot_by_tag(tag.to_string(), pool).await?; + let snapshot = query_snapshot_by_tag(tag.clone(), pool).await?; let mut voter_info = Vec::new(); let voters = - query_voters_by_voting_key_and_snapshot_tag(id.to_hex(), tag.to_string(), pool).await?; + query_voters_by_voting_key_and_snapshot_tag(voting_key.clone(), tag.clone(), pool).await?; + for voter in voters { - let contributors = query_contributors_by_voting_key_and_voter_group_and_snapshot_tag( - id.to_hex(), + let contributors = query_contributions_by_voting_key_and_voter_group_and_snapshot_tag( + voting_key.clone(), voter.voting_group.clone(), - tag.to_string(), + tag.clone(), pool, ) .await?; + + let total_voting_power_per_group = + query_total_voting_power_by_voting_group_and_snapshot_tag( + voter.voting_group.clone(), + tag.clone(), + pool, + ) + .await? as f64; + voter_info.push(VoterInfo { voting_power: Value::from(voter.voting_power as u64), delegations_count: contributors.len() as u64, @@ -73,6 +88,11 @@ pub async fn get_voters_info( .map(|contributor| contributor.value as u64) .sum(), voting_group: voter.voting_group, + voting_power_saturation: if total_voting_power_per_group != 0_f64 { + voter.voting_power as f64 / total_voting_power_per_group + } else { + 0_f64 + }, }) } @@ -82,7 +102,46 @@ pub async fn get_voters_info( }) } -pub async fn get_tags(context: SharedContext_) -> Result, HandleError> { +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct DelegatorInfo { + pub dreps: Vec, + pub voting_groups: Vec, + /// Timestamp for the latest update in voter info in the current snapshot + #[serde(deserialize_with = "crate::utils::serde::deserialize_unix_timestamp_from_rfc3339")] + #[serde(serialize_with = "crate::utils::serde::serialize_unix_timestamp_as_rfc3339")] + pub last_updated: i64, +} + +#[tracing::instrument(skip(context))] +pub async fn get_delegator_info( + tag: String, + stake_public_key: String, + context: SharedContext, +) -> Result { + let pool = &context.read().await.db_connection_pool; + + let snapshot = query_snapshot_by_tag(tag.clone(), pool).await?; + + let contributions = + query_contributions_by_stake_public_key_and_snapshot_tag(stake_public_key, tag, pool) + .await?; + + Ok(DelegatorInfo { + dreps: contributions + .iter() + .map(|contribution| contribution.voting_key.clone()) + .unique() + .collect(), + voting_groups: contributions + .iter() + .map(|contribution| contribution.voting_group.clone()) + .unique() + .collect(), + last_updated: snapshot.last_updated, + }) +} + +pub async fn get_tags(context: SharedContext) -> Result, HandleError> { let pool = &context.read().await.db_connection_pool; Ok(query_all_snapshots(pool) @@ -95,14 +154,14 @@ pub async fn get_tags(context: SharedContext_) -> Result, HandleError> #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip(snapshot, context))] pub async fn update_from_raw_snapshot( - tag: &str, + tag: String, snapshot: RawSnapshot, - update_timestamp: u64, + update_timestamp: i64, min_stake_threshold: Value, voting_power_cap: Fraction, direct_voters_group: Option, representatives_group: Option, - context: SharedContext_, + context: SharedContext, ) -> Result<(), HandleError> { let direct_voter = direct_voters_group.unwrap_or_else(|| DEFAULT_DIRECT_VOTER_GROUP.into()); let representative = @@ -118,19 +177,17 @@ pub async fn update_from_raw_snapshot( #[tracing::instrument(skip(snapshot, context))] pub async fn update_from_shanpshot_info( - tag: &str, + tag: String, snapshot: impl IntoIterator, - update_timestamp: u64, - context: SharedContext_, + update_timestamp: i64, + context: SharedContext, ) -> Result<(), HandleError> { let pool = &context.read().await.db_connection_pool; put_snapshot( models::snapshot::Snapshot { - tag: tag.to_string(), - last_updated: update_timestamp - .try_into() - .expect("value should not exceed i64 limit"), + tag: tag.clone(), + last_updated: update_timestamp, }, pool, )?; @@ -139,7 +196,7 @@ pub async fn update_from_shanpshot_info( let mut voters = Vec::new(); for entry in snapshot.into_iter() { contributions.extend(entry.contributions.into_iter().map(|contribution| { - Contributor { + Contribution { stake_public_key: contribution.stake_public_key, reward_address: contribution.reward_address, value: contribution @@ -148,22 +205,18 @@ pub async fn update_from_shanpshot_info( .expect("value should not exceed i64 limit"), voting_key: entry.hir.voting_key.to_hex(), voting_group: entry.hir.voting_group.clone(), - snapshot_tag: tag.to_string(), + snapshot_tag: tag.clone(), } - .values() })); - voters.push( - Voter { - voting_key: entry.hir.voting_key.to_hex(), - voting_group: entry.hir.voting_group.clone(), - voting_power: Into::::into(entry.hir.voting_power) - .try_into() - .expect("value should not exceed i64 limit"), - snapshot_tag: tag.to_string(), - } - .values(), - ); + voters.push(Voter { + voting_key: entry.hir.voting_key.to_hex(), + voting_group: entry.hir.voting_group.clone(), + voting_power: Into::::into(entry.hir.voting_power) + .try_into() + .expect("value should not exceed i64 limit"), + snapshot_tag: tag.clone(), + }); } let db_conn = pool.get().map_err(HandleError::DatabaseError)?; batch_put_voters(&voters, &db_conn)?; @@ -198,6 +251,10 @@ mod test { "1111111111111111111111111111111111111111111111111111111111111111", ) .unwrap(), + Identifier::from_hex( + "2222222222222222222222222222222222222222222222222222222222222222", + ) + .unwrap(), ]; const GROUP1: &str = "group1"; @@ -206,24 +263,34 @@ mod test { const TAG1: &str = "tag1"; const TAG2: &str = "tag2"; - const UPDATE_TIME1: u64 = 0; - const UPDATE_TIME2: u64 = 1; + const UPDATE_TIME1: i64 = 0; + const UPDATE_TIME2: i64 = 1; let key_0_values = [ VoterInfo { voting_group: GROUP1.to_string(), voting_power: Value::from(1), - delegations_power: 0, - delegations_count: 0, + delegations_power: 2, + delegations_count: 2, + voting_power_saturation: 1_f64 / 3_f64, }, VoterInfo { voting_group: GROUP2.to_string(), voting_power: Value::from(2), - delegations_power: 0, - delegations_count: 0, + delegations_power: 2, + delegations_count: 2, + voting_power_saturation: 1_f64, }, ]; + let key_1_values = [VoterInfo { + voting_group: GROUP1.to_string(), + voting_power: Value::from(2), + delegations_power: 2, + delegations_count: 2, + voting_power_saturation: 2_f64 / 3_f64, + }]; + let content_a = std::iter::repeat(keys[0].clone()) .take(key_0_values.len()) .zip(key_0_values.iter().cloned()) @@ -235,9 +302,21 @@ mod test { voting_power, delegations_power: _, delegations_count: _, + voting_power_saturation: _, }, )| SnapshotInfo { - contributions: vec![], + contributions: vec![ + KeyContribution { + reward_address: "address_1".to_string(), + stake_public_key: "stake_public_key_1".to_string(), + value: 1, + }, + KeyContribution { + reward_address: "address_2".to_string(), + stake_public_key: "stake_public_key_2".to_string(), + value: 1, + }, + ], hir: VoterHIR { voting_key, voting_group, @@ -245,22 +324,63 @@ mod test { }, }, ) + .chain( + std::iter::repeat(keys[1].clone()) + .take(key_1_values.len()) + .zip(key_1_values.iter().cloned()) + .map( + |( + voting_key, + VoterInfo { + voting_group, + voting_power, + delegations_power: _, + delegations_count: _, + voting_power_saturation: _, + }, + )| SnapshotInfo { + contributions: vec![ + KeyContribution { + reward_address: "address_1".to_string(), + stake_public_key: "stake_public_key_1".to_string(), + value: 1, + }, + KeyContribution { + reward_address: "address_2".to_string(), + stake_public_key: "stake_public_key_2".to_string(), + value: 1, + }, + ], + hir: VoterHIR { + voting_key, + voting_group, + voting_power, + }, + }, + ), + ) .collect::>(); - update_from_shanpshot_info(TAG1, content_a.clone(), UPDATE_TIME1, context.clone()) - .await - .unwrap(); + update_from_shanpshot_info( + TAG1.to_string(), + content_a.clone(), + UPDATE_TIME1, + context.clone(), + ) + .await + .unwrap(); - let key_1_values = [VoterInfo { + let key_2_values = [VoterInfo { voting_group: GROUP1.to_string(), voting_power: Value::from(3), delegations_power: 0, delegations_count: 0, + voting_power_saturation: 0.5_f64, }]; - let content_b = std::iter::repeat(keys[1].clone()) - .take(key_1_values.len()) - .zip(key_1_values.iter().cloned()) + let content_b = std::iter::repeat(keys[2].clone()) + .take(key_2_values.len()) + .zip(key_2_values.iter().cloned()) .map( |( voting_key, @@ -269,6 +389,7 @@ mod test { voting_power, delegations_power: _, delegations_count: _, + voting_power_saturation: _, }, )| SnapshotInfo { contributions: vec![], @@ -282,7 +403,7 @@ mod test { .collect::>(); update_from_shanpshot_info( - TAG2, + TAG2.to_string(), [content_a, content_b].concat(), UPDATE_TIME2, context.clone(), @@ -292,25 +413,103 @@ mod test { assert_eq!( &key_0_values[..], - &super::get_voters_info(TAG1, &keys[0], context.clone()) + &super::get_voters_info(TAG1.to_string(), keys[0].to_hex(), context.clone()) .await .unwrap() .voter_info[..], ); - assert!(&super::get_voters_info(TAG1, &keys[1], context.clone()) - .await - .unwrap() - .voter_info - .is_empty(),); - assert_eq!( &key_1_values[..], - &super::get_voters_info(TAG2, &keys[1], context) + &super::get_voters_info(TAG1.to_string(), keys[1].to_hex(), context.clone()) + .await + .unwrap() + .voter_info[..], + ); + + assert!( + &super::get_voters_info(TAG1.to_string(), keys[2].to_hex(), context.clone()) + .await + .unwrap() + .voter_info + .is_empty(), + ); + + assert_eq!( + &key_2_values[..], + &super::get_voters_info(TAG2.to_string(), keys[2].to_hex(), context.clone()) .await .unwrap() .voter_info[..], ); + + assert_eq!( + super::get_delegator_info( + TAG1.to_string(), + "stake_public_key_1".to_string(), + context.clone() + ) + .await + .unwrap(), + DelegatorInfo { + dreps: vec![ + "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + "1111111111111111111111111111111111111111111111111111111111111111".to_string() + ], + voting_groups: vec!["group1".to_string(), "group2".to_string()], + last_updated: UPDATE_TIME1, + } + ); + + assert_eq!( + super::get_delegator_info( + TAG1.to_string(), + "stake_public_key_2".to_string(), + context.clone() + ) + .await + .unwrap(), + DelegatorInfo { + dreps: vec![ + "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + "1111111111111111111111111111111111111111111111111111111111111111".to_string() + ], + voting_groups: vec!["group1".to_string(), "group2".to_string()], + last_updated: UPDATE_TIME1, + } + ); + + assert_eq!( + super::get_delegator_info( + TAG2.to_string(), + "stake_public_key_1".to_string(), + context.clone() + ) + .await + .unwrap(), + DelegatorInfo { + dreps: vec![ + "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + "1111111111111111111111111111111111111111111111111111111111111111".to_string() + ], + voting_groups: vec!["group1".to_string(), "group2".to_string()], + last_updated: UPDATE_TIME2, + } + ); + + assert_eq!( + super::get_delegator_info(TAG2.to_string(), "stake_public_key_2".to_string(), context) + .await + .unwrap(), + DelegatorInfo { + dreps: vec![ + "0000000000000000000000000000000000000000000000000000000000000000".to_string(), + "1111111111111111111111111111111111111111111111111111111111111111".to_string() + ], + voting_groups: vec!["group1".to_string(), "group2".to_string()], + last_updated: UPDATE_TIME2, + } + ); } #[tokio::test] @@ -318,7 +517,7 @@ mod test { const TAG1: &str = "tag1"; const TAG2: &str = "tag2"; - const UPDATE_TIME1: u64 = 0; + const UPDATE_TIME1: i64 = 0; let context = new_in_memmory_db_test_shared_context(); let db_conn = &context.read().await.db_connection_pool.get().unwrap(); @@ -348,15 +547,25 @@ mod test { }, ]; - update_from_shanpshot_info(TAG1, inputs.clone(), UPDATE_TIME1, context.clone()) - .await - .unwrap(); - update_from_shanpshot_info(TAG2, inputs.clone(), UPDATE_TIME1, context.clone()) - .await - .unwrap(); + update_from_shanpshot_info( + TAG1.to_string(), + inputs.clone(), + UPDATE_TIME1, + context.clone(), + ) + .await + .unwrap(); + update_from_shanpshot_info( + TAG2.to_string(), + inputs.clone(), + UPDATE_TIME1, + context.clone(), + ) + .await + .unwrap(); assert_eq!( - super::get_voters_info(TAG1, &voting_key, context.clone()) + super::get_voters_info(TAG1.to_string(), voting_key.to_hex(), context.clone()) .await .unwrap() .voter_info, @@ -371,13 +580,14 @@ mod test { .iter() .map(|KeyContribution { value, .. }| value) .sum(), - delegations_count: snapshot.contributions.len() as u64 + delegations_count: snapshot.contributions.len() as u64, + voting_power_saturation: 1_f64, }) .collect::>() ); super::update_from_shanpshot_info( - TAG1, + TAG1.to_string(), inputs[0..1].to_vec(), UPDATE_TIME1, context.clone(), @@ -386,7 +596,7 @@ mod test { .unwrap(); assert_eq!( - super::get_voters_info(TAG1, &voting_key, context.clone()) + super::get_voters_info(TAG1.to_string(), voting_key.to_hex(), context.clone()) .await .unwrap() .voter_info, @@ -401,14 +611,15 @@ mod test { .iter() .map(|KeyContribution { value, .. }| value) .sum(), - delegations_count: snapshot.contributions.len() as u64 + delegations_count: snapshot.contributions.len() as u64, + voting_power_saturation: 1_f64, }) .collect::>() ); // asserting that TAG2 is untouched, just in case assert_eq!( - super::get_voters_info(TAG2, &voting_key, context.clone()) + super::get_voters_info(TAG2.to_string(), voting_key.to_hex(), context.clone()) .await .unwrap() .voter_info, @@ -423,7 +634,8 @@ mod test { .iter() .map(|KeyContribution { value, .. }| value) .sum(), - delegations_count: snapshot.contributions.len() as u64 + delegations_count: snapshot.contributions.len() as u64, + voting_power_saturation: 1_f64, }) .collect::>() ); @@ -439,7 +651,7 @@ mod test { F::Extract: Reply + Send, { let result = warp::test::request() - .path(format!("/snapshot/{}/{}", tag, voting_key).as_ref()) + .path(format!("/snapshot/voter/{}/{}", tag, voting_key).as_ref()) .reply(filter) .await; diff --git a/vit-servicing-station-lib/src/v0/endpoints/snapshot/routes.rs b/vit-servicing-station-lib/src/v0/endpoints/snapshot/routes.rs index 424d3428..b2ea66e2 100644 --- a/vit-servicing-station-lib/src/v0/endpoints/snapshot/routes.rs +++ b/vit-servicing-station-lib/src/v0/endpoints/snapshot/routes.rs @@ -1,6 +1,8 @@ use crate::v0::context::SharedContext; -use super::handlers::{get_tags, get_voters_info, put_raw_snapshot, put_snapshot_info}; +use super::handlers::{ + get_delegator_info, get_tags, get_voters_info, put_raw_snapshot, put_snapshot_info, +}; use warp::filters::BoxedFilter; use warp::{Filter, Rejection, Reply}; @@ -10,17 +12,22 @@ pub fn filter( ) -> impl Filter + Clone { let with_context = warp::any().map(move || context.clone()); - let get_voters_info = warp::path!(String / String) + let get_voters_info = warp::path!("voter" / String / String) .and(warp::get()) .and(with_context.clone()) .and_then(get_voters_info); + let get_delegator_info = warp::path!("delegator" / String / String) + .and(warp::get()) + .and(with_context.clone()) + .and_then(get_delegator_info); + let get_tags = warp::path::end() .and(warp::get()) .and(with_context) .and_then(get_tags); - root.and(get_voters_info.or(get_tags)) + root.and(get_voters_info.or(get_delegator_info).or(get_tags)) } pub fn update_filter( diff --git a/vit-servicing-station-tests/src/common/clients/rest/path.rs b/vit-servicing-station-tests/src/common/clients/rest/path.rs index 191f3055..6e05dda5 100644 --- a/vit-servicing-station-tests/src/common/clients/rest/path.rs +++ b/vit-servicing-station-tests/src/common/clients/rest/path.rs @@ -46,7 +46,7 @@ impl RestPathBuilder { } pub fn snapshot_voter_info(&self, tag: &str, key: &str) -> String { - self.path(&format!("snapshot/{}/{}", tag, key)) + self.path(&format!("snapshot/voter/{}/{}", tag, key)) } pub fn proposal(&self, id: &str, group: &str) -> String { diff --git a/vit-servicing-station-tests/src/common/snapshot.rs b/vit-servicing-station-tests/src/common/snapshot.rs index 37b169cd..bc4b93d4 100644 --- a/vit-servicing-station-tests/src/common/snapshot.rs +++ b/vit-servicing-station-tests/src/common/snapshot.rs @@ -24,7 +24,7 @@ pub struct SnapshotBuilder { groups: Vec, voters_count: usize, contributions_count: usize, - update_timestamp: u64, + update_timestamp: i64, } impl Default for SnapshotBuilder { @@ -34,7 +34,7 @@ impl Default for SnapshotBuilder { groups: vec!["direct".to_string(), "dreps".to_string()], voters_count: 3, contributions_count: 5, - update_timestamp: OffsetDateTime::now_utc().unix_timestamp() as u64, + update_timestamp: OffsetDateTime::now_utc().unix_timestamp(), } } } @@ -60,7 +60,7 @@ impl SnapshotBuilder { self } - pub fn with_timestamp(mut self, timestamp: u64) -> Self { + pub fn with_timestamp(mut self, timestamp: i64) -> Self { self.update_timestamp = timestamp; self } @@ -114,7 +114,13 @@ impl SnapshotBuilder { #[derive(Deserialize, Serialize, Debug, PartialEq, Eq, Clone)] pub struct VoterInfo { - pub last_updated: u64, + #[serde( + deserialize_with = "vit_servicing_station_lib::utils::serde::deserialize_unix_timestamp_from_rfc3339" + )] + #[serde( + serialize_with = "vit_servicing_station_lib::utils::serde::serialize_unix_timestamp_as_rfc3339" + )] + pub last_updated: i64, pub voter_info: Vec, }