diff --git a/Cargo.lock b/Cargo.lock index 52a926d7..6926837e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -906,6 +906,17 @@ dependencies = [ "serde", ] +[[package]] +name = "bigdecimal" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6773ddc0eafc0e509fb60e48dff7f450f8e674a0686ae8605e8d9901bd5eefa" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "bitcoin-internals" version = "0.2.0" @@ -7189,6 +7200,7 @@ source = "git+https://github.com/helikon-labs/sqlx.git?branch=helikon-increased- dependencies = [ "ahash 0.8.11", "atoi", + "bigdecimal", "byteorder", "bytes", "chrono", @@ -7268,6 +7280,7 @@ source = "git+https://github.com/helikon-labs/sqlx.git?branch=helikon-increased- dependencies = [ "atoi", "base64 0.21.7", + "bigdecimal", "bitflags 2.5.0", "byteorder", "bytes", @@ -7310,6 +7323,7 @@ source = "git+https://github.com/helikon-labs/sqlx.git?branch=helikon-increased- dependencies = [ "atoi", "base64 0.21.7", + "bigdecimal", "bitflags 2.5.0", "byteorder", "chrono", @@ -7328,6 +7342,7 @@ dependencies = [ "log", "md-5", "memchr", + "num-bigint", "once_cell", "rand 0.8.5", "serde", @@ -7850,6 +7865,7 @@ dependencies = [ "actix-web", "anyhow", "async-trait", + "chrono", "futures-util", "hex", "lazy_static", @@ -7859,6 +7875,7 @@ dependencies = [ "rustc-hash", "serde", "serde_json", + "sqlx", "subvt-config", "subvt-logging", "subvt-metrics", @@ -7990,6 +8007,7 @@ dependencies = [ "hex", "lazy_static", "log", + "num-traits", "pallet-conviction-voting", "pallet-democracy", "pallet-identity", diff --git a/Cargo.toml b/Cargo.toml index 3f2cfe3b..e75d577d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,4 +34,5 @@ anyhow = "1.0" itertools = "0.12.0" lazy_static = "1.4" log = "0.4" +num-traits = "0.2" thiserror = "1.0" \ No newline at end of file diff --git a/subvt-network-status-updater/src/lib.rs b/subvt-network-status-updater/src/lib.rs index 18b18235..e5ffe214 100644 --- a/subvt-network-status-updater/src/lib.rs +++ b/subvt-network-status-updater/src/lib.rs @@ -38,7 +38,7 @@ impl NetworkStatusUpdater { ))?; let status_json_string = serde_json::to_string(status)?; let mut redis_cmd_pipeline = Pipeline::new(); - redis_cmd_pipeline + () = redis_cmd_pipeline .cmd("SET") .arg(format!("subvt:{}:network_status", CONFIG.substrate.chain)) .arg(status_json_string) diff --git a/subvt-notification-generator/src/lib.rs b/subvt-notification-generator/src/lib.rs index ebd1594f..04221c87 100644 --- a/subvt-notification-generator/src/lib.rs +++ b/subvt-notification-generator/src/lib.rs @@ -1,9 +1,9 @@ //! Generates notifications according to the notification rules depending on three sources of data: //! 1. Validator list updates from Redis, updated by `subvt-validator-list-updater`, and published -//! using the Redis notification (PUBLISH) support. +//! using the Redis notification (PUBLISH) support. //! 2. Events and extrinsics in new blocks. Block are processed by `subvt-block-processor`, and the -//! finishing of the processing of a block is signalled by the processor by means of PostgreSQL -//! notifications. +//! finishing of the processing of a block is signalled by the processor by means of PostgreSQL +//! notifications. //! 3. Regular Telemetry checks (this is work in progress still). #![warn(clippy::disallowed_types)] use async_trait::async_trait; diff --git a/subvt-persistence/src/postgres/app/notification.rs b/subvt-persistence/src/postgres/app/notification.rs index da913b3c..1eb32f8d 100644 --- a/subvt-persistence/src/postgres/app/notification.rs +++ b/subvt-persistence/src/postgres/app/notification.rs @@ -148,7 +148,7 @@ impl PostgreSQLAppStorage { .bind(¬ification.validator_account_json) .bind(¬ification.notification_type_code) .bind(notification.user_notification_channel_id as i32) - .bind(¬ification.notification_channel.to_string()) + .bind(notification.notification_channel.to_string()) .bind(¬ification.notification_target) .bind(¬ification.data_json) .bind(¬ification.error_log) diff --git a/subvt-persistence/src/postgres/app/user.rs b/subvt-persistence/src/postgres/app/user.rs index e3081fc1..a47f8a9e 100644 --- a/subvt-persistence/src/postgres/app/user.rs +++ b/subvt-persistence/src/postgres/app/user.rs @@ -218,7 +218,7 @@ impl PostgreSQLAppStorage { "#, ) .bind(user_notification_channel.user_id as i32) - .bind(&user_notification_channel.channel.to_string()) + .bind(user_notification_channel.channel.to_string()) .bind(&user_notification_channel.target) .fetch_one(&self.connection_pool) .await?; @@ -237,7 +237,7 @@ impl PostgreSQLAppStorage { "#, ) .bind(user_notification_channel.user_id as i32) - .bind(&user_notification_channel.channel.to_string()) + .bind(user_notification_channel.channel.to_string()) .bind(&user_notification_channel.target) .fetch_one(&self.connection_pool) .await?; diff --git a/subvt-persistence/src/postgres/network/kline/mod.rs b/subvt-persistence/src/postgres/network/kline/mod.rs new file mode 100644 index 00000000..325ecaec --- /dev/null +++ b/subvt-persistence/src/postgres/network/kline/mod.rs @@ -0,0 +1,106 @@ +use crate::postgres::network::PostgreSQLNetworkStorage; +use sqlx::types::BigDecimal; +use subvt_types::kline::KLine; + +type DBKline = ( + i32, + i64, + String, + String, + BigDecimal, + BigDecimal, + BigDecimal, + BigDecimal, + BigDecimal, + i64, + BigDecimal, + i32, + BigDecimal, + BigDecimal, +); + +impl PostgreSQLNetworkStorage { + pub async fn kline_exists( + &self, + timestamp: i64, + source_ticker: &str, + target_ticker: &str, + ) -> anyhow::Result { + let record_count: (i64,) = sqlx::query_as( + r#" + SELECT COUNT(id) FROM sub_kline_historical + WHERE open_time = $1 AND source_ticker = $2 AND target_ticker = $3 + "#, + ) + .bind(timestamp) + .bind(source_ticker) + .bind(target_ticker) + .fetch_one(&self.connection_pool) + .await?; + Ok(record_count.0 > 0) + } + + pub async fn save_kline(&self, kline: &KLine) -> anyhow::Result { + let save_result: (i32,) = sqlx::query_as( + r#" + INSERT INTO sub_kline_historical (open_time, source_ticker, target_ticker, "open", high, low, "close", volume, close_time, quote_volume, "count", taker_buy_volume, taker_buy_quote_volume) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) + ON CONFLICT (open_time, source_ticker, target_ticker) + DO UPDATE SET taker_buy_quote_volume = EXCLUDED.taker_buy_quote_volume + RETURNING id + "#, + ) + .bind(kline.open_time as i64) + .bind(kline.source_ticker.as_str()) + .bind(kline.target_ticker.as_str()) + .bind(&kline.open) + .bind(&kline.high) + .bind(&kline.low) + .bind(&kline.close) + .bind(&kline.volume) + .bind(kline.close_time as i64) + .bind(&kline.quote_volume) + .bind(kline.count as i32) + .bind(&kline.taker_buy_volume) + .bind(&kline.taker_buy_quote_volume) + .fetch_one(&self.connection_pool) + .await?; + Ok(save_result.0) + } + + pub async fn get_kline_count(&self) -> anyhow::Result { + let record_count: (i64,) = sqlx::query_as("SELECT COUNT(id) FROM sub_kline_historical") + .fetch_one(&self.connection_pool) + .await?; + Ok(record_count.0 as u64) + } + + pub async fn get_kline(&self, timestamp: u64) -> anyhow::Result { + let db_kline: DBKline = sqlx::query_as( + r#" + SELECT id, open_time, source_ticker, target_ticker, "open", high, low, "close", volume, close_time, quote_volume, "count", taker_buy_volume, taker_buy_quote_volume + FROM sub_kline_historical + WHERE open_time = $1 + "#, + ) + .bind(timestamp as i64) + .fetch_one(&self.connection_pool) + .await?; + Ok(KLine { + id: db_kline.0 as u32, + open_time: db_kline.1 as u64, + source_ticker: db_kline.2, + target_ticker: db_kline.3, + open: db_kline.4, + high: db_kline.5, + low: db_kline.6, + close: db_kline.7, + volume: db_kline.8, + close_time: db_kline.9 as u64, + quote_volume: db_kline.10, + count: db_kline.11 as u32, + taker_buy_volume: db_kline.12, + taker_buy_quote_volume: db_kline.13, + }) + } +} diff --git a/subvt-persistence/src/postgres/network/mod.rs b/subvt-persistence/src/postgres/network/mod.rs index 530b8f9e..a78674ac 100644 --- a/subvt-persistence/src/postgres/network/mod.rs +++ b/subvt-persistence/src/postgres/network/mod.rs @@ -12,6 +12,7 @@ pub mod era; pub mod error_log; pub mod event; pub mod extrinsic; +pub mod kline; pub mod nft; pub mod notify; pub mod onekv; diff --git a/subvt-persistence/src/postgres/network/onekv.rs b/subvt-persistence/src/postgres/network/onekv.rs index ca44a460..d34c25e5 100644 --- a/subvt-persistence/src/postgres/network/onekv.rs +++ b/subvt-persistence/src/postgres/network/onekv.rs @@ -48,7 +48,7 @@ impl PostgreSQLNetworkStorage { .bind(candidate.inclusion) .bind(candidate.commission) .bind(candidate.is_active) - .bind(&candidate.unclaimed_eras.as_ref().map(|v| v.iter().map(|i| *i as i64).collect::>())) + .bind(candidate.unclaimed_eras.as_ref().map(|v| v.iter().map(|i| *i as i64).collect::>())) .bind(candidate.nominated_at.map(|last_valid| last_valid as i64)) .bind(candidate.offline_accumulated) .bind(candidate.offline_since as i64) diff --git a/subvt-persistence/src/postgres/network/report/rewards.rs b/subvt-persistence/src/postgres/network/report/rewards.rs index ecdfba23..aaf689b5 100644 --- a/subvt-persistence/src/postgres/network/report/rewards.rs +++ b/subvt-persistence/src/postgres/network/report/rewards.rs @@ -1,7 +1,7 @@ use crate::postgres::network::PostgreSQLNetworkStorage; use std::str::FromStr; use subvt_types::crypto::AccountId; -use subvt_types::report::ValidatorTotalReward; +use subvt_types::report::{Reward, ValidatorTotalReward}; use subvt_types::substrate::{Balance, Era}; impl PostgreSQLNetworkStorage { @@ -70,4 +70,41 @@ impl PostgreSQLNetworkStorage { } Ok(result) } + + pub async fn get_rewards_in_time_range( + &self, + rewardee_account_id: &AccountId, + start_timestamp: u64, + end_timestamp: u64, + ) -> anyhow::Result> { + let db_rewards: Vec<(i32, String, i64, i64, i32, i32, String)> = sqlx::query_as( + r#" + SELECT E.id, E.block_hash, B.number, B.timestamp, E.extrinsic_index, E.event_index, E.amount + FROM sub_event_rewarded E + INNER JOIN sub_block B ON B.hash = E.block_hash + WHERE B.timestamp >= $1 AND B.timestamp < $2 + AND E.rewardee_account_id = $3 + ORDER BY B.timestamp ASC + "#, + ) + .bind(start_timestamp as i64) + .bind(end_timestamp as i64) + .bind(rewardee_account_id.to_string()) + .fetch_all(&self.connection_pool) + .await?; + let mut rewards = Vec::new(); + for db_reward in db_rewards.iter() { + rewards.push(Reward { + id: db_reward.0 as u32, + block_hash: db_reward.1.clone(), + block_number: db_reward.2 as u64, + block_timestamp: db_reward.3 as u64, + extrinsic_index: db_reward.4 as u32, + event_index: db_reward.5 as u32, + rewardee_account_id: *rewardee_account_id, + amount: db_reward.6.parse()?, + }) + } + Ok(rewards) + } } diff --git a/subvt-persistence/src/postgres/network/telegram/mod.rs b/subvt-persistence/src/postgres/network/telegram/mod.rs index 0173841f..b8a9183c 100644 --- a/subvt-persistence/src/postgres/network/telegram/mod.rs +++ b/subvt-persistence/src/postgres/network/telegram/mod.rs @@ -253,7 +253,7 @@ impl PostgreSQLNetworkStorage { ) .bind(app_user_id as i32) .bind(telegram_chat_id) - .bind(&state.to_string()) + .bind(state.to_string()) .execute(&self.connection_pool) .await?; Ok(()) @@ -284,7 +284,7 @@ impl PostgreSQLNetworkStorage { "#, ) .bind(telegram_chat_id) - .bind(&validator_account_id.to_string()) + .bind(validator_account_id.to_string()) .fetch_one(&self.connection_pool) .await?; Ok(record_count.0 > 0) @@ -307,7 +307,7 @@ impl PostgreSQLNetworkStorage { "#, ) .bind(telegram_chat_id) - .bind(&account_id.to_string()) + .bind(account_id.to_string()) .bind(address) .bind(display) .fetch_one(&self.connection_pool) @@ -348,7 +348,7 @@ impl PostgreSQLNetworkStorage { WHERE telegram_chat_id = $2 "#, ) - .bind(&state.to_string()) + .bind(state.to_string()) .bind(telegram_chat_id) .execute(&self.connection_pool) .await?; diff --git a/subvt-persistence/src/redis.rs b/subvt-persistence/src/redis.rs index dbbb77a3..b4f5fe64 100644 --- a/subvt-persistence/src/redis.rs +++ b/subvt-persistence/src/redis.rs @@ -28,7 +28,7 @@ impl Redis { block_summary: &BlockSummary, ) -> anyhow::Result<()> { let mut connection = self.client.get_multiplexed_async_connection().await?; - redis::cmd("MSET") + () = redis::cmd("MSET") .arg(format!( "subvt:{}:validators:finalized_block_number", CONFIG.substrate.chain @@ -55,7 +55,7 @@ impl Redis { account_id: &AccountId, ) -> anyhow::Result<()> { let mut connection = self.client.get_multiplexed_async_connection().await?; - redis::cmd("SADD") + () = redis::cmd("SADD") .arg(format!( "subvt:{}:validators:{finalized_block_number}:active:account_id_set", CONFIG.substrate.chain, @@ -73,7 +73,7 @@ impl Redis { ) -> anyhow::Result<()> { let mut connection = self.client.get_multiplexed_async_connection().await?; let validator_details_json = serde_json::to_string(validator_details)?; - redis::cmd("SET") + () = redis::cmd("SET") .arg(format!( "subvt:{}:validators:{finalized_block_number}:active:validator:{}", CONFIG.substrate.chain, validator_details.account.id, @@ -171,7 +171,7 @@ impl Redis { pub async fn set_network_status(&self, network_status: &NetworkStatus) -> anyhow::Result<()> { let mut connection = self.client.get_multiplexed_async_connection().await?; let network_status_json = serde_json::to_string(network_status)?; - redis::cmd("SET") + () = redis::cmd("SET") .arg(format!("subvt:{}:network_status", CONFIG.substrate.chain)) .arg(network_status_json) .query_async(&mut connection) diff --git a/subvt-report-service/Cargo.toml b/subvt-report-service/Cargo.toml index e721b171..abc4a3a3 100644 --- a/subvt-report-service/Cargo.toml +++ b/subvt-report-service/Cargo.toml @@ -8,6 +8,7 @@ rust-version = "1.67.0" actix-web = "4.4" anyhow = { workspace = true } async-trait = "0.1" +chrono = "0.4" futures-util = "0.3" hex = "0.4" lazy_static = { workspace = true } @@ -17,6 +18,7 @@ redis = { version = "0.25", features = ["tokio-comp"] } rustc-hash = "1.1.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" +sqlx = { git = "https://github.com/helikon-labs/sqlx.git", branch = "helikon-increased-field-count", features = ["bigdecimal"] } subvt-config = { path = "../subvt-config" } subvt-metrics = { path = "../subvt-metrics" } subvt-persistence = { path = "../subvt-persistence" } diff --git a/subvt-report-service/src/lib.rs b/subvt-report-service/src/lib.rs index 532d1dd9..9fab7767 100644 --- a/subvt-report-service/src/lib.rs +++ b/subvt-report-service/src/lib.rs @@ -228,6 +228,7 @@ impl Service for ReportService { .service(validator::validator_era_rewards_service) .service(validator::validator_era_payouts_service) .service(validator::validator_reward_chart_service) + .service(validator::validator_monhtly_income_service) .service(staking::controller_service) .service(staking::bond_service) .service(network::get_network_status) diff --git a/subvt-report-service/src/network/mod.rs b/subvt-report-service/src/network/mod.rs index e21f2e4f..c3698ac0 100644 --- a/subvt-report-service/src/network/mod.rs +++ b/subvt-report-service/src/network/mod.rs @@ -4,4 +4,4 @@ use actix_web::{get, web, HttpResponse}; #[get("/network/status")] pub(crate) async fn get_network_status(data: web::Data) -> ResultResponse { Ok(HttpResponse::Ok().json(data.redis.get_network_status().await?)) -} \ No newline at end of file +} diff --git a/subvt-report-service/src/validator/mod.rs b/subvt-report-service/src/validator/mod.rs index 87def4f1..c5af7bc7 100644 --- a/subvt-report-service/src/validator/mod.rs +++ b/subvt-report-service/src/validator/mod.rs @@ -1,13 +1,15 @@ use crate::{ResultResponse, ServiceState, CONFIG}; use actix_web::{get, web, HttpResponse}; +use chrono::{DateTime, Datelike, Months, NaiveDateTime, Utc}; use serde::Deserialize; use std::str::FromStr; use subvt_substrate_client::SubstrateClient; use subvt_types::crypto::AccountId; use subvt_types::err::ServiceError; use subvt_types::report::{ - BlockSummary, EraValidatorPayoutReport, EraValidatorRewardReport, ValidatorDetailsReport, - ValidatorListReport, ValidatorSummaryReport, ValidatorTotalRewardChartData, + BlockSummary, EraValidatorPayoutReport, EraValidatorRewardReport, MonthlyIncome, + ValidatorDetailsReport, ValidatorListReport, ValidatorSummaryReport, + ValidatorTotalRewardChartData, }; use subvt_types::subvt::{ValidatorSearchSummary, ValidatorSummary}; @@ -310,3 +312,57 @@ pub(crate) async fn validator_reward_chart_service( end_timestamp: query.end_timestamp, })) } + +#[get("/validator/{ss58_address_or_account_id}/income/monthly")] +pub(crate) async fn validator_monhtly_income_service( + path: web::Path, + data: web::Data, +) -> ResultResponse { + let account_id = match validate_path_param(&path.into_inner().ss58_address_or_account_id) { + Ok(account_id) => account_id, + Err(response) => return Ok(response), + }; + let now = Utc::now(); + let start_date = now + .checked_sub_months(Months::new(12)) + .unwrap() + .date_naive() + .with_day(1) + .unwrap(); + let start_timestamp = NaiveDateTime::from(start_date).and_utc().timestamp_millis(); + let end_timestamp = now.timestamp_millis(); + let rewards = data + .postgres + .get_rewards_in_time_range(&account_id, start_timestamp as u64, end_timestamp as u64) + .await?; + let mut monthly_incomes: Vec = Vec::new(); + let denominator = f64::powi(10.0, CONFIG.substrate.token_decimals as i32); + for reward in rewards.iter() { + let reward_day = DateTime::from_timestamp_millis(reward.block_timestamp as i64) + .unwrap() + .date_naive(); + let reward_year = reward_day.year() as u32; + let reward_month = reward_day.month(); + let reward_day_begin_timestamp = + NaiveDateTime::from(reward_day).and_utc().timestamp_millis(); + let kline_close = data + .postgres + .get_kline(reward_day_begin_timestamp as u64) + .await? + .close_to_f64() + .unwrap(); + let reward = (reward.amount as f64) * kline_close / denominator; + if let Some(monthly_income) = monthly_incomes.iter_mut().find(|monthly_income| { + monthly_income.month == reward_month && monthly_income.year == reward_year + }) { + monthly_income.income += reward; + } else { + monthly_incomes.push(MonthlyIncome { + year: reward_year, + month: reward_month, + income: reward, + }); + } + } + Ok(HttpResponse::Ok().json(monthly_incomes)) +} diff --git a/subvt-types/Cargo.toml b/subvt-types/Cargo.toml index ce0aa0b7..9241847c 100644 --- a/subvt-types/Cargo.toml +++ b/subvt-types/Cargo.toml @@ -14,6 +14,7 @@ frame-system = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = " hex = "0.4" lazy_static = { workspace = true } log = { workspace = true } +num-traits = { workspace = true } pallet-conviction-voting = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.11.0" } pallet-democracy = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.11.0" } pallet-identity = { git = "https://github.com/paritytech/polkadot-sdk.git", tag = "polkadot-v1.11.0" } diff --git a/subvt-types/src/kline.rs b/subvt-types/src/kline.rs new file mode 100644 index 00000000..24fa3b9c --- /dev/null +++ b/subvt-types/src/kline.rs @@ -0,0 +1,26 @@ +use num_traits::ToPrimitive; +pub use sqlx::types::BigDecimal; + +#[derive(Clone, Debug)] +pub struct KLine { + pub id: u32, + pub open_time: u64, + pub source_ticker: String, + pub target_ticker: String, + pub open: BigDecimal, + pub high: BigDecimal, + pub low: BigDecimal, + pub close: BigDecimal, + pub volume: BigDecimal, + pub close_time: u64, + pub quote_volume: BigDecimal, + pub count: u32, + pub taker_buy_volume: BigDecimal, + pub taker_buy_quote_volume: BigDecimal, +} + +impl KLine { + pub fn close_to_f64(&self) -> Option { + self.close.to_f64() + } +} diff --git a/subvt-types/src/lib.rs b/subvt-types/src/lib.rs index 3de76658..c5fa4af7 100644 --- a/subvt-types/src/lib.rs +++ b/subvt-types/src/lib.rs @@ -4,6 +4,7 @@ pub mod app; pub mod crypto; pub mod err; pub mod governance; +pub mod kline; pub mod onekv; pub mod rdb; pub mod report; diff --git a/subvt-types/src/report.rs b/subvt-types/src/report.rs index d6d96b20..9c0b2f01 100644 --- a/subvt-types/src/report.rs +++ b/subvt-types/src/report.rs @@ -220,3 +220,22 @@ pub struct Bond { pub controller_address: String, pub bond: Stake, } + +#[derive(Clone, Debug)] +pub struct Reward { + pub id: u32, + pub block_hash: String, + pub block_number: u64, + pub block_timestamp: u64, + pub extrinsic_index: u32, + pub event_index: u32, + pub rewardee_account_id: AccountId, + pub amount: Balance, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct MonthlyIncome { + pub year: u32, + pub month: u32, + pub income: f64, +} diff --git a/subvt-validator-list-updater/src/lib.rs b/subvt-validator-list-updater/src/lib.rs index 020e4de9..3e55f3d4 100644 --- a/subvt-validator-list-updater/src/lib.rs +++ b/subvt-validator-list-updater/src/lib.rs @@ -149,7 +149,7 @@ impl ValidatorListUpdater { )) .arg(finalized_block_number); log::info!("Write to Redis."); - redis_cmd_pipeline + () = redis_cmd_pipeline .query_async(&mut redis_connection) .await .context("Error while setting Redis validators.")?; @@ -192,7 +192,7 @@ impl ValidatorListUpdater { } processed_block_numbers.remove(0); } - redis_cmd_pipeline + () = redis_cmd_pipeline .query_async(&mut redis_connection) .await .context("Error while setting Redis validators.")?; @@ -282,7 +282,7 @@ impl ValidatorListUpdater { "Cannot connect to Redis at URL {}.", CONFIG.redis.url ))?; - redis::cmd("SET") + () = redis::cmd("SET") .arg(&[ format!( "subvt:{}:validators:processed_block_numbers",