From b386a62c51bce2ccabb094a4cced0f5cd6b90fc3 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 28 Apr 2021 15:38:33 +0300 Subject: [PATCH 01/23] primitives - Config - change creators_whitelist to Address - primitives - Campaign - Validators don't rely on positioning for their role --- primitives/src/campaign.rs | 63 +++++++++-------------------- primitives/src/channel_validator.rs | 4 +- primitives/src/config.rs | 5 +-- sentry/src/spender.rs | 6 +-- 4 files changed, 26 insertions(+), 52 deletions(-) diff --git a/primitives/src/campaign.rs b/primitives/src/campaign.rs index c4691ed7c..76e3ba486 100644 --- a/primitives/src/campaign.rs +++ b/primitives/src/campaign.rs @@ -35,6 +35,7 @@ mod campaign_id { pub struct CampaignId([u8; 16]); impl CampaignId { + /// Generates randomly a `CampaignId` using `Uuid::new_v4().to_simple()` pub fn new() -> Self { Self::default() } @@ -184,10 +185,10 @@ pub struct Campaign { } impl Campaign { - pub fn find_validator(&self, validator: ValidatorId) -> Option<&'_ ValidatorDesc> { + pub fn find_validator(&self, validator: &ValidatorId) -> Option> { match (self.leader(), self.follower()) { - (Some(leader), _) if leader.id == validator => Some(leader), - (_, Some(follower)) if follower.id == validator => Some(follower), + (Some(leader), _) if &leader.id == validator => Some(ValidatorRole::Leader(leader)), + (_, Some(follower)) if &follower.id == validator => Some(ValidatorRole::Follower(follower)), _ => None, } } @@ -195,21 +196,13 @@ impl Campaign { /// Matches the Channel.leader to the Campaign.spec.leader /// If they match it returns `Some`, otherwise, it returns `None` pub fn leader(&self) -> Option<&'_ ValidatorDesc> { - if self.channel.leader == self.validators.leader().id { - Some(self.validators.leader()) - } else { - None - } + self.validators.find(&self.channel.leader) } /// Matches the Channel.follower to the Campaign.spec.follower /// If they match it returns `Some`, otherwise, it returns `None` pub fn follower(&self) -> Option<&'_ ValidatorDesc> { - if self.channel.follower == self.validators.follower().id { - Some(self.validators.follower()) - } else { - None - } + self.validators.find(&self.channel.follower) } /// Returns the pricing of a given event @@ -289,10 +282,10 @@ pub mod validators { use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] - /// A (leader, follower) tuple + /// Unordered list of the validators representing the leader & follower pub struct Validators(ValidatorDesc, ValidatorDesc); - #[derive(Debug)] + #[derive(Debug, PartialEq, Eq, Clone)] pub enum ValidatorRole<'a> { Leader(&'a ValidatorDesc), Follower(&'a ValidatorDesc), @@ -308,33 +301,15 @@ pub mod validators { } impl Validators { - pub fn new(leader: ValidatorDesc, follower: ValidatorDesc) -> Self { - Self(leader, follower) - } - - pub fn leader(&self) -> &ValidatorDesc { - &self.0 - } - - pub fn follower(&self) -> &ValidatorDesc { - &self.1 - } - - pub fn find(&self, validator_id: &ValidatorId) -> Option> { - if &self.leader().id == validator_id { - Some(ValidatorRole::Leader(&self.leader())) - } else if &self.follower().id == validator_id { - Some(ValidatorRole::Follower(&self.follower())) - } else { - None - } + pub fn new(validators: (ValidatorDesc, ValidatorDesc)) -> Self { + Self(validators.0, validators.1) } - pub fn find_index(&self, validator_id: &ValidatorId) -> Option { - if &self.leader().id == validator_id { - Some(0) - } else if &self.follower().id == validator_id { - Some(1) + pub fn find(&self, validator_id: &ValidatorId) -> Option<&ValidatorDesc> { + if &self.0.id == validator_id { + Some(&self.0) + } else if &self.1.id == validator_id { + Some(&self.1) } else { None } @@ -346,8 +321,8 @@ pub mod validators { } impl From<(ValidatorDesc, ValidatorDesc)> for Validators { - fn from((leader, follower): (ValidatorDesc, ValidatorDesc)) -> Self { - Self(leader, follower) + fn from(validators: (ValidatorDesc, ValidatorDesc)) -> Self { + Self(validators.0, validators.1) } } @@ -383,12 +358,12 @@ pub mod validators { 0 => { self.index += 1; - Some(self.validators.leader()) + Some(&self.validators.0) } 1 => { self.index += 1; - Some(self.validators.follower()) + Some(&self.validators.1) } _ => None, } diff --git a/primitives/src/channel_validator.rs b/primitives/src/channel_validator.rs index 18b1e91e6..b33f5cb13 100644 --- a/primitives/src/channel_validator.rs +++ b/primitives/src/channel_validator.rs @@ -7,7 +7,7 @@ use std::cmp::PartialEq; use time::Duration; // -// TODO: AIP#61 How relevant is this validator? Check and remove if it's obsolete +// TODO: AIP#61 OBSOLETE, remove once we remove the Channel Create!How relevant is this validator? Check and remove if it's obsolete // pub trait ChannelValidator { fn is_channel_valid( @@ -45,7 +45,7 @@ pub trait ChannelValidator { return Err(ChannelError::UnlistedValidator); } - if !creator_listed(&channel, &config.creators_whitelist) { + if !creator_listed(&channel, &config.creators_whitelist.iter().map(Into::into).collect::>()) { return Err(ChannelError::UnlistedCreator); } diff --git a/primitives/src/config.rs b/primitives/src/config.rs index 5c7de047e..7d91f6888 100644 --- a/primitives/src/config.rs +++ b/primitives/src/config.rs @@ -1,5 +1,4 @@ -use crate::event_submission::RateLimit; -use crate::{BigNum, ValidatorId}; +use crate::{event_submission::RateLimit, Address, BigNum, ValidatorId}; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; use serde_hex::{SerHex, StrictPfx}; @@ -31,7 +30,7 @@ pub struct Config { pub validator_tick_timeout: u32, pub ip_rate_limit: RateLimit, // HashMap?? pub sid_rate_limit: RateLimit, // HashMap ?? - pub creators_whitelist: Vec, + pub creators_whitelist: Vec
, pub minimal_deposit: BigNum, pub minimal_fee: BigNum, pub token_address_whitelist: Vec, diff --git a/sentry/src/spender.rs b/sentry/src/spender.rs index 2e7f3d20c..da6b9cacd 100644 --- a/sentry/src/spender.rs +++ b/sentry/src/spender.rs @@ -36,11 +36,11 @@ pub mod fee { campaign: &Campaign, for_validator: ValidatorId, ) -> Result, DomainError> { - let payout = match campaign.find_validator(for_validator) { - Some(validator) => { + let payout = match campaign.find_validator(&for_validator) { + Some(validator_role) => { // should never overflow let fee_payout = payout - .checked_mul(&validator.fee) + .checked_mul(&validator_role.validator().fee) .ok_or_else(|| { DomainError::InvalidArgument("payout calculation overflow".to_string()) })? From dd2a007100bfb80540978810b433c16cf526fedf Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 28 Apr 2021 15:39:41 +0300 Subject: [PATCH 02/23] primitives - WIP campaign_validator --- primitives/src/campaign_validator.rs | 122 +++++++++++++++++++++++++++ primitives/src/lib.rs | 1 + 2 files changed, 123 insertions(+) create mode 100644 primitives/src/campaign_validator.rs diff --git a/primitives/src/campaign_validator.rs b/primitives/src/campaign_validator.rs new file mode 100644 index 000000000..f9115bbc6 --- /dev/null +++ b/primitives/src/campaign_validator.rs @@ -0,0 +1,122 @@ +use crate::{Address, Campaign, UnifiedNum, ValidatorId, campaign::Validators, config::Config}; +use chrono::Utc; +use std::cmp::PartialEq; +use thiserror::Error; + +pub trait Validator { + fn validate( + &self, + config: &Config, + validator_identity: &ValidatorId, + ) -> Result; +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum Validation { + Ok, + /// When the Adapter address is not listed in the `campaign.validators` & `campaign.channel.(leader/follower)` + /// which in terms means, that the adapter shouldn't handle this Campaign + AdapterNotIncluded, + /// when `channel.active.to` has passed (i.e. < now), the Campaign should not be handled + // campaign.active.to must be in the future + InvalidActiveTo, + UnlistedValidator, + UnlistedCreator, + UnlistedAsset, + MinimumDepositNotMet, + MinimumValidatorFeeNotMet, + FeeConstraintViolated, +} + +#[derive(Debug, Eq, PartialEq, Clone, Copy, Error)] +pub enum Error { + #[error("Summing the Validators fee results in overflow")] + FeeSumOverflow, +} + +impl Validator for Campaign { + fn validate( + &self, + config: &Config, + validator_identity: &ValidatorId, + ) -> Result { + + // check if the channel validators include our adapter identity + let whoami_validator = match self.find_validator(validator_identity) { + Some(role) => role.validator(), + None => return Ok(Validation::AdapterNotIncluded) + }; + + if self.active.to < Utc::now() { + return Ok(Validation::InvalidActiveTo); + } + + if !all_validators_listed(&self.validators, &config.validators_whitelist) { + return Ok(Validation::UnlistedValidator); + } + + if !creator_listed(&self, &config.creators_whitelist) { + return Ok(Validation::UnlistedCreator); + } + + if !asset_listed(&self, &config.token_address_whitelist) { + return Ok(Validation::UnlistedAsset); + } + + // TODO AIP#61: Use configuration to check the minimum deposit of the token! + if self.budget < UnifiedNum::from(500) { + return Ok(Validation::MinimumDepositNotMet); + } + + // TODO AIP#61: Use Configuration to check the minimum validator fee of the token! + if whoami_validator.fee < UnifiedNum::from(100) { + return Ok(Validation::MinimumValidatorFeeNotMet); + } + + let total_validator_fee: UnifiedNum = self + .validators + .iter() + .map(|v| &v.fee) + .sum::>() + // on overflow return an error + .ok_or_else(|| Error::FeeSumOverflow)?; + + if total_validator_fee >= self.budget { + return Ok(Validation::FeeConstraintViolated); + } + + Ok(Validation::Ok) + } +} + +pub fn all_validators_listed(validators: &Validators, whitelist: &[ValidatorId]) -> bool { + if whitelist.is_empty() { + true + } else { + let found_validators = whitelist + .iter() + .filter(|&allowed| { + validators.find(allowed).is_some() + }) + // this will ensure that if we find the 2 validators earlier + // we don't go over the other values of the whitelist + .take(2); + // the found validators should be exactly 2, if they are not, then 1 or 2 are missing + found_validators.count() == 2 + } +} + +pub fn creator_listed(campaign: &Campaign, whitelist: &[Address]) -> bool { + // if the list is empty, return true, as we don't have a whitelist to restrict us to + // or if we have a list, check if it includes the `channel.creator` + whitelist.is_empty() || whitelist.iter().any(|allowed| allowed.eq(&campaign.creator)) +} + +pub fn asset_listed(campaign: &Campaign, whitelist: &[String]) -> bool { + // if the list is empty, return true, as we don't have a whitelist to restrict us to + // or if we have a list, check if it includes the `channel.deposit_asset` + whitelist.is_empty() + || whitelist + .iter() + .any(|allowed| allowed == &campaign.channel.token.to_string()) +} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 8f99c5abe..d9c93a176 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -28,6 +28,7 @@ pub mod campaign; pub mod channel; pub mod channel_v5; pub mod channel_validator; +pub mod campaign_validator; pub mod config; mod eth_checksum; pub mod event_submission; From 29e7f7a03981d37153457e18a78711414b64d564 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 28 Apr 2021 16:13:56 +0300 Subject: [PATCH 03/23] rustfmt & primitives - fix prep_db --- primitives/src/campaign_validator.rs | 14 +++++++------- primitives/src/channel_validator.rs | 9 ++++++++- primitives/src/lib.rs | 2 +- primitives/src/util/tests/prep_db.rs | 2 +- 4 files changed, 17 insertions(+), 10 deletions(-) diff --git a/primitives/src/campaign_validator.rs b/primitives/src/campaign_validator.rs index f9115bbc6..8cad34158 100644 --- a/primitives/src/campaign_validator.rs +++ b/primitives/src/campaign_validator.rs @@ -1,4 +1,4 @@ -use crate::{Address, Campaign, UnifiedNum, ValidatorId, campaign::Validators, config::Config}; +use crate::{campaign::Validators, config::Config, Address, Campaign, UnifiedNum, ValidatorId}; use chrono::Utc; use std::cmp::PartialEq; use thiserror::Error; @@ -40,11 +40,10 @@ impl Validator for Campaign { config: &Config, validator_identity: &ValidatorId, ) -> Result { - // check if the channel validators include our adapter identity let whoami_validator = match self.find_validator(validator_identity) { Some(role) => role.validator(), - None => return Ok(Validation::AdapterNotIncluded) + None => return Ok(Validation::AdapterNotIncluded), }; if self.active.to < Utc::now() { @@ -95,9 +94,7 @@ pub fn all_validators_listed(validators: &Validators, whitelist: &[ValidatorId]) } else { let found_validators = whitelist .iter() - .filter(|&allowed| { - validators.find(allowed).is_some() - }) + .filter(|&allowed| validators.find(allowed).is_some()) // this will ensure that if we find the 2 validators earlier // we don't go over the other values of the whitelist .take(2); @@ -109,7 +106,10 @@ pub fn all_validators_listed(validators: &Validators, whitelist: &[ValidatorId]) pub fn creator_listed(campaign: &Campaign, whitelist: &[Address]) -> bool { // if the list is empty, return true, as we don't have a whitelist to restrict us to // or if we have a list, check if it includes the `channel.creator` - whitelist.is_empty() || whitelist.iter().any(|allowed| allowed.eq(&campaign.creator)) + whitelist.is_empty() + || whitelist + .iter() + .any(|allowed| allowed.eq(&campaign.creator)) } pub fn asset_listed(campaign: &Campaign, whitelist: &[String]) -> bool { diff --git a/primitives/src/channel_validator.rs b/primitives/src/channel_validator.rs index b33f5cb13..2d8338c5c 100644 --- a/primitives/src/channel_validator.rs +++ b/primitives/src/channel_validator.rs @@ -45,7 +45,14 @@ pub trait ChannelValidator { return Err(ChannelError::UnlistedValidator); } - if !creator_listed(&channel, &config.creators_whitelist.iter().map(Into::into).collect::>()) { + if !creator_listed( + &channel, + &config + .creators_whitelist + .iter() + .map(Into::into) + .collect::>(), + ) { return Err(ChannelError::UnlistedCreator); } diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index d9c93a176..8089710f0 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -25,10 +25,10 @@ pub mod analytics; pub mod balances_map; pub mod big_num; pub mod campaign; +pub mod campaign_validator; pub mod channel; pub mod channel_v5; pub mod channel_validator; -pub mod campaign_validator; pub mod config; mod eth_checksum; pub mod event_submission; diff --git a/primitives/src/util/tests/prep_db.rs b/primitives/src/util/tests/prep_db.rs index 48cbd5124..01fdb7943 100644 --- a/primitives/src/util/tests/prep_db.rs +++ b/primitives/src/util/tests/prep_db.rs @@ -98,7 +98,7 @@ lazy_static! { creator: IDS["creator"].to_address(), // 1000.00000000 budget: UnifiedNum::from(100_000_000_000), - validators: Validators::new(DUMMY_VALIDATOR_LEADER.clone(), DUMMY_VALIDATOR_FOLLOWER.clone()), + validators: Validators::new((DUMMY_VALIDATOR_LEADER.clone(), DUMMY_VALIDATOR_FOLLOWER.clone())), title: Some("Dummy Campaign".to_string()), pricing_bounds: Some(campaign::PricingBounds {impression: Some(campaign::Pricing { min: 1.into(), max: 10.into()}), click: Some(campaign::Pricing { min: 0.into(), max: 0.into()})}), event_submission: Some(EventSubmission { allow: vec![] }), From 35fe2a22b01482256b4daf63dbf016dff021a4f9 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 28 Apr 2021 16:21:03 +0300 Subject: [PATCH 04/23] primtivies - campaign impls of FromSql, ToSql & Row: - prim - Campaign - From - prim - campaing - Validators - From/ToSql - prim - campaing - PricingBounds - From/ToSql - prim - EventSubmission - From/ToSql - prim - targeting - eval - Rules - FromSql - prim - AdUnit - From/ToSql --- primitives/src/ad_unit.rs | 31 ++++++++ primitives/src/campaign.rs | 109 +++++++++++++++++++++++++++-- primitives/src/event_submission.rs | 32 +++++++++ primitives/src/targeting/eval.rs | 12 +++- 4 files changed, 178 insertions(+), 6 deletions(-) diff --git a/primitives/src/ad_unit.rs b/primitives/src/ad_unit.rs index 521ff2147..d9a5e4b56 100644 --- a/primitives/src/ad_unit.rs +++ b/primitives/src/ad_unit.rs @@ -52,3 +52,34 @@ pub struct AdUnit { )] pub modified: Option>, } + +#[cfg(feature = "postgres")] +mod postgres { + use super::AdUnit; + + use bytes::BytesMut; + use postgres_types::{accepts, to_sql_checked, FromSql, IsNull, Json, ToSql, Type}; + use std::error::Error; + impl<'a> FromSql<'a> for AdUnit { + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + let json = as FromSql>::from_sql(ty, raw)?; + + Ok(json.0) + } + + accepts!(JSONB); + } + + impl ToSql for AdUnit { + fn to_sql( + &self, + ty: &Type, + w: &mut BytesMut, + ) -> Result> { + Json(self).to_sql(ty, w) + } + + accepts!(JSONB); + to_sql_checked!(); + } +} diff --git a/primitives/src/campaign.rs b/primitives/src/campaign.rs index 76e3ba486..1efad5e2a 100644 --- a/primitives/src/campaign.rs +++ b/primitives/src/campaign.rs @@ -188,7 +188,9 @@ impl Campaign { pub fn find_validator(&self, validator: &ValidatorId) -> Option> { match (self.leader(), self.follower()) { (Some(leader), _) if &leader.id == validator => Some(ValidatorRole::Leader(leader)), - (_, Some(follower)) if &follower.id == validator => Some(ValidatorRole::Follower(follower)), + (_, Some(follower)) if &follower.id == validator => { + Some(ValidatorRole::Follower(follower)) + } _ => None, } } @@ -282,7 +284,7 @@ pub mod validators { use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq)] - /// Unordered list of the validators representing the leader & follower + /// Unordered list of the validators representing the leader & follower pub struct Validators(ValidatorDesc, ValidatorDesc); #[derive(Debug, PartialEq, Eq, Clone)] @@ -371,6 +373,103 @@ pub mod validators { } } -// TODO: Postgres Campaign -// TODO: Postgres CampaignSpec -// TODO: Postgres Validators +#[cfg(feature = "postgres")] +mod postgres { + + use super::{Active, Campaign, CampaignId, PricingBounds, Validators}; + use bytes::BytesMut; + use postgres_types::{accepts, to_sql_checked, FromSql, IsNull, Json, ToSql, Type}; + use std::error::Error; + use tokio_postgres::Row; + + impl From<&Row> for Campaign { + fn from(row: &Row) -> Self { + Self { + id: row.get("id"), + channel: row.get("channel"), + creator: row.get("creator"), + budget: row.get("budget"), + validators: row.get("validators"), + title: row.get("title"), + pricing_bounds: row.get("pricing_bounds"), + event_submission: row.get("event_submission"), + ad_units: row.get("ad_units"), + targeting_rules: row.get("targeting_rules"), + created: row.get("created"), + active: Active { + from: row.get("active_from"), + to: row.get("active_to"), + }, + } + } + } + + impl<'a> FromSql<'a> for CampaignId { + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + let str_slice = <&str as FromSql>::from_sql(ty, raw)?; + + Ok(str_slice.parse()?) + } + + accepts!(TEXT, VARCHAR); + } + + impl ToSql for CampaignId { + fn to_sql( + &self, + ty: &Type, + w: &mut BytesMut, + ) -> Result> { + self.to_string().to_sql(ty, w) + } + + accepts!(TEXT, VARCHAR); + to_sql_checked!(); + } + + impl<'a> FromSql<'a> for Validators { + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + let json = as FromSql>::from_sql(ty, raw)?; + + Ok(json.0) + } + + accepts!(JSONB); + } + + impl ToSql for Validators { + fn to_sql( + &self, + ty: &Type, + w: &mut BytesMut, + ) -> Result> { + Json(self).to_sql(ty, w) + } + + accepts!(JSONB); + to_sql_checked!(); + } + + impl<'a> FromSql<'a> for PricingBounds { + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + let json = as FromSql>::from_sql(ty, raw)?; + + Ok(json.0) + } + + accepts!(JSONB); + } + + impl ToSql for PricingBounds { + fn to_sql( + &self, + ty: &Type, + w: &mut BytesMut, + ) -> Result> { + Json(self).to_sql(ty, w) + } + + accepts!(JSONB); + to_sql_checked!(); + } +} diff --git a/primitives/src/event_submission.rs b/primitives/src/event_submission.rs index d6205c615..d76c38845 100644 --- a/primitives/src/event_submission.rs +++ b/primitives/src/event_submission.rs @@ -26,3 +26,35 @@ pub struct RateLimit { #[serde(rename = "timeframe", with = "serde_millis")] pub time_frame: Duration, } + +#[cfg(feature = "postgres")] +mod postgres { + use super::EventSubmission; + + use bytes::BytesMut; + use postgres_types::{accepts, to_sql_checked, FromSql, IsNull, Json, ToSql, Type}; + use std::error::Error; + + impl<'a> FromSql<'a> for EventSubmission { + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + let json = as FromSql>::from_sql(ty, raw)?; + + Ok(json.0) + } + + accepts!(JSONB); + } + + impl ToSql for EventSubmission { + fn to_sql( + &self, + ty: &Type, + w: &mut BytesMut, + ) -> Result> { + Json(self).to_sql(ty, w) + } + + accepts!(JSONB); + to_sql_checked!(); + } +} diff --git a/primitives/src/targeting/eval.rs b/primitives/src/targeting/eval.rs index 023916881..29fe9cdbd 100644 --- a/primitives/src/targeting/eval.rs +++ b/primitives/src/targeting/eval.rs @@ -1288,7 +1288,7 @@ fn math_operator(lhs: Number, rhs: Number, ops: MathOperator) -> Result FromSql<'a> for Rules { + fn from_sql(ty: &Type, raw: &'a [u8]) -> Result> { + let json = as FromSql>::from_sql(ty, raw)?; + + Ok(json.0) + } + + accepts!(JSONB); + } } From cd691a6c959351f54a7d3cf3e66394d8ee5b9d16 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 29 Apr 2021 13:25:52 +0300 Subject: [PATCH 05/23] sentry - db - campaign - insert_campaign --- sentry/src/db.rs | 1 + sentry/src/db/campaign.rs | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 sentry/src/db/campaign.rs diff --git a/sentry/src/db.rs b/sentry/src/db.rs index 81a336788..0207ecc07 100644 --- a/sentry/src/db.rs +++ b/sentry/src/db.rs @@ -6,6 +6,7 @@ use tokio_postgres::NoTls; use lazy_static::lazy_static; pub mod analytics; +mod campaign; mod channel; pub mod event_aggregate; pub mod spendable; diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs new file mode 100644 index 000000000..5783b6573 --- /dev/null +++ b/sentry/src/db/campaign.rs @@ -0,0 +1,33 @@ +use crate::db::{DbPool, PoolError}; +use primitives::Campaign; + +pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result { + let client = pool.get().await?; + + let stmt = client.prepare("INSERT INTO campaigns (id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)").await?; + + let row = client + .execute( + &stmt, + &[ + &campaign.id, + &campaign.channel.id(), + &campaign.channel, + &campaign.creator, + &campaign.budget, + &campaign.validators, + &campaign.title, + &campaign.pricing_bounds, + &campaign.event_submission, + &campaign.ad_units, + &campaign.targeting_rules, + &campaign.created, + &campaign.active.from, + &campaign.active.to, + ], + ) + .await?; + + let inserted = row == 1; + Ok(inserted) +} From 1c6e49bb2103eb895d8cc8a2d11aded45e9105de Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 29 Apr 2021 13:26:17 +0300 Subject: [PATCH 06/23] WIP sentry - routes - campaign create --- primitives/src/sentry.rs | 66 +++++++++++++++++++++++++++++++++++ sentry/src/routes/campaign.rs | 44 +++++++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 sentry/src/routes/campaign.rs diff --git a/primitives/src/sentry.rs b/primitives/src/sentry.rs index 630ec5ca0..51e51d559 100644 --- a/primitives/src/sentry.rs +++ b/primitives/src/sentry.rs @@ -315,6 +315,72 @@ pub mod channel_list { } } +pub mod campaign_create { + use chrono::{serde::ts_milliseconds, DateTime, Utc}; + use serde::{Deserialize, Serialize}; + use serde_with::with_prefix; + + use crate::{ + campaign::{Active, PricingBounds, Validators}, + channel_v5::Channel, + targeting::Rules, + AdUnit, Address, Campaign, CampaignId, EventSubmission, UnifiedNum, + }; + + // use the same prefix for Active + with_prefix!(prefix_active "active_"); + + #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] + /// All fields are present except the `CampaignId` which is randomly created + /// This struct defines the Body of the request (in JSON) + pub struct CreateCampaign { + pub channel: Channel, + pub creator: Address, + pub budget: UnifiedNum, + pub validators: Validators, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub title: Option, + /// Event pricing bounds + #[serde(default, skip_serializing_if = "Option::is_none")] + pub pricing_bounds: Option, + /// EventSubmission object, applies to event submission (POST /channel/:id/events) + #[serde(default, skip_serializing_if = "Option::is_none")] + pub event_submission: Option, + /// An array of AdUnit (optional) + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub ad_units: Vec, + #[serde(default)] + pub targeting_rules: Rules, + /// A millisecond timestamp of when the campaign was created + #[serde(with = "ts_milliseconds")] + pub created: DateTime, + /// A millisecond timestamp representing the time you want this campaign to become active (optional) + /// Used by the AdViewManager & Targeting AIP#31 + #[serde(flatten, with = "prefix_active")] + pub active: Active, + } + + impl CreateCampaign { + /// Creates the new `Campaign` with randomly generated `CampaignId` + pub fn to_campaign(self) -> Campaign { + Campaign { + id: CampaignId::new(), + channel: self.channel, + creator: self.creator, + budget: self.budget, + validators: self.validators, + title: self.title, + pricing_bounds: self.pricing_bounds, + event_submission: self.event_submission, + ad_units: self.ad_units, + targeting_rules: self.targeting_rules, + created: self.created, + active: self.active, + } + } + } +} + #[cfg(feature = "postgres")] mod postgres { use super::{MessageResponse, ValidatorMessage}; diff --git a/sentry/src/routes/campaign.rs b/sentry/src/routes/campaign.rs new file mode 100644 index 000000000..3753b5096 --- /dev/null +++ b/sentry/src/routes/campaign.rs @@ -0,0 +1,44 @@ +use crate::{success_response, Application, Auth, ResponseError, RouteParams, Session}; +use hyper::{Body, Request, Response}; +use primitives::{adapter::Adapter, sentry::{ + campaign_create::CreateCampaign,SuccessResponse}}; + +pub async fn create_campaign( + req: Request, + app: &Application, +) -> Result, ResponseError> { + let body = hyper::body::to_bytes(req.into_body()).await?; + + let campaign = serde_json::from_slice::(&body) + .map_err(|e| ResponseError::FailedValidation(e.to_string()))? + // create the actual `Campaign` with random `CampaignId` + .to_campaign(); + + + // TODO AIP#61: Validate Campaign + + let error_response = ResponseError::BadRequest("err occurred; please try again later".into()); + + // insert Campaign + + // match insert_campaign(&app.pool, &campaign).await { + // Err(error) => { + // error!(&app.logger, "{}", &error; "module" => "create_channel"); + + // match error { + // PoolError::Backend(error) if error.code() == Some(&SqlState::UNIQUE_VIOLATION) => { + // Err(ResponseError::Conflict( + // "channel already exists".to_string(), + // )) + // } + // _ => Err(error_response), + // } + // } + // Ok(false) => Err(error_response), + // _ => Ok(()), + // }?; + + let create_response = SuccessResponse { success: true }; + + Ok(success_response(serde_json::to_string(&campaign)?)) +} From 5c74e893b92b0df49b419459f61d8541ae3f2c50 Mon Sep 17 00:00:00 2001 From: simzzz Date: Wed, 12 May 2021 16:22:59 +0300 Subject: [PATCH 07/23] Added fetch campaign implementation + postgres integration tests --- primitives/src/campaign_validator.rs | 2 +- sentry/src/db/campaign.rs | 93 +++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/primitives/src/campaign_validator.rs b/primitives/src/campaign_validator.rs index 8cad34158..d1e1c460a 100644 --- a/primitives/src/campaign_validator.rs +++ b/primitives/src/campaign_validator.rs @@ -78,7 +78,7 @@ impl Validator for Campaign { .map(|v| &v.fee) .sum::>() // on overflow return an error - .ok_or_else(|| Error::FeeSumOverflow)?; + .ok_or(Error::FeeSumOverflow)?; if total_validator_fee >= self.budget { return Ok(Validation::FeeConstraintViolated); diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index 5783b6573..2aca087f4 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -1,5 +1,6 @@ use crate::db::{DbPool, PoolError}; -use primitives::Campaign; +use primitives::{channel::ChannelId, Campaign}; +use std::convert::TryFrom; pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result { let client = pool.get().await?; @@ -31,3 +32,93 @@ pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result Result { + let client = pool.get().await?; + let statement = client.prepare("SELECT id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to FROM campaigns WHERE id = $1 AND channel_id = $2").await?; + + let row = client + .query_one( + &statement, + &[&campaign.id, &ChannelId::from(campaign.channel.id())], + ) + .await?; + + Ok(Campaign::try_from(&row)?) +} + +#[cfg(feature = "postgres")] +mod postgres { + use std::convert::TryFrom; + use tokio_postgres::{Error, Row}; + + use super::*; + + impl TryFrom<&Row> for Campaign { + type Error = Error; + + fn try_from(row: Row) -> Result { + Ok(Campaign { + id: row.try_get("id")?, + channel: row.try_get("channel")?, + creator: row.try_get("creator")?, + budget: row.try_get("budget")?, + validators: row.try_get("validators")?, + title: row.try_get("title")?, + pricing_bounds: row.try_get("pricing_bounds")?, + event_submission: row.try_get("event_submission")?, + ad_units: row.try_get("ad_units")?, + targeting_rules: row.try_get("targeting_rules")?, + created: row.try_get("created")?, + creator: row.try_get("creator")?, + creator: row.try_get("creator")?, + active: Active { + to: row.try_get("active_to"), + from: row.try_get("active_from"), + }, + }) + } + } +} + +#[cfg(test)] +mod test { + use primitives::{ + campaign::{Campaign, CampaignId}, + channel_v5::Channel, + util::tests::prep_db::{ADDRESSES, DUMMY_CAMPAIGN}, + ChannelId, UnifiedNum, + }; + + use crate::db::{ + tests_postgres::{setup_test_migrations, test_postgres_connection}, + POSTGRES_CONFIG, + }; + + use super::*; + + #[tokio::test] + async fn it_inserts_and_fetches_campaign() { + let test_pool = test_postgres_connection(POSTGRES_CONFIG.clone()) + .get() + .await + .unwrap(); + + setup_test_migrations(test_pool.clone()) + .await + .expect("Migrations should succeed"); + + let campaign_for_testing = DUMMY_CAMPAIGN.clone(); + let is_inserted = insert_campaign(&test_pool.clone(), &campaign_for_testing) + .await + .expect("Should succeed"); + + assert!(is_inserted); + + let fetched_campaign: Campaign = fetch_campaign(test_pool.clone(), &campaign_for_testing) + .await + .expect("Should fetch successfully"); + + assert_eq!(campaign_for_testing, fetched_campaign); + } +} From d0eadc8321a5b90e167b604f569257cafc037cca Mon Sep 17 00:00:00 2001 From: simzzz Date: Wed, 12 May 2021 17:58:23 +0300 Subject: [PATCH 08/23] removed a field which was retrieved twice --- sentry/src/db/campaign.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index 2aca087f4..4ea969fdc 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -33,6 +33,10 @@ pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result Result { let client = pool.get().await?; let statement = client.prepare("SELECT id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to FROM campaigns WHERE id = $1 AND channel_id = $2").await?; @@ -57,7 +61,7 @@ mod postgres { impl TryFrom<&Row> for Campaign { type Error = Error; - fn try_from(row: Row) -> Result { + fn try_from(row: &Row) -> Result { Ok(Campaign { id: row.try_get("id")?, channel: row.try_get("channel")?, @@ -70,8 +74,6 @@ mod postgres { ad_units: row.try_get("ad_units")?, targeting_rules: row.try_get("targeting_rules")?, created: row.try_get("created")?, - creator: row.try_get("creator")?, - creator: row.try_get("creator")?, active: Active { to: row.try_get("active_to"), from: row.try_get("active_from"), From cfffdac5b522552708f924552b92a74c904509be Mon Sep 17 00:00:00 2001 From: simzzz Date: Wed, 12 May 2021 18:01:36 +0300 Subject: [PATCH 09/23] Imported active --- sentry/src/db/campaign.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index 4ea969fdc..f188eb634 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -55,6 +55,7 @@ pub async fn fetch_campaign(pool: DbPool, campaign: &Campaign) -> Result Date: Thu, 13 May 2021 17:19:12 +0300 Subject: [PATCH 10/23] removed try_from for from --- sentry/src/db/campaign.rs | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index f188eb634..3bafb2caf 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -48,40 +48,7 @@ pub async fn fetch_campaign(pool: DbPool, campaign: &Campaign) -> Result for Campaign { - type Error = Error; - - fn try_from(row: &Row) -> Result { - Ok(Campaign { - id: row.try_get("id")?, - channel: row.try_get("channel")?, - creator: row.try_get("creator")?, - budget: row.try_get("budget")?, - validators: row.try_get("validators")?, - title: row.try_get("title")?, - pricing_bounds: row.try_get("pricing_bounds")?, - event_submission: row.try_get("event_submission")?, - ad_units: row.try_get("ad_units")?, - targeting_rules: row.try_get("targeting_rules")?, - created: row.try_get("created")?, - active: Active { - to: row.try_get("active_to"), - from: row.try_get("active_from"), - }, - }) - } - } + Ok(Campaign::from(&row)) } #[cfg(test)] From 71abe12146149bb330ca33c1af733afe50bbea93 Mon Sep 17 00:00:00 2001 From: simzzz Date: Mon, 17 May 2021 18:08:28 +0300 Subject: [PATCH 11/23] comitting progress --- sentry/src/db/campaign.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index 3bafb2caf..190216382 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -1,12 +1,12 @@ use crate::db::{DbPool, PoolError}; -use primitives::{channel::ChannelId, Campaign}; +use primitives::{channel::ChannelId, AdUnit, Campaign}; use std::convert::TryFrom; +use tokio_postgres::types::Json; pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result { let client = pool.get().await?; - + let ad_units: Json> = Json(campaign.ad_units.clone()); let stmt = client.prepare("INSERT INTO campaigns (id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)").await?; - let row = client .execute( &stmt, @@ -20,7 +20,7 @@ pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result Date: Mon, 17 May 2021 18:17:50 +0300 Subject: [PATCH 12/23] renamed function because of clippy --- primitives/src/sentry.rs | 2 +- sentry/src/routes/campaign.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/primitives/src/sentry.rs b/primitives/src/sentry.rs index 51e51d559..2c776dfe7 100644 --- a/primitives/src/sentry.rs +++ b/primitives/src/sentry.rs @@ -362,7 +362,7 @@ pub mod campaign_create { impl CreateCampaign { /// Creates the new `Campaign` with randomly generated `CampaignId` - pub fn to_campaign(self) -> Campaign { + pub fn convert_to_campaign(self) -> Campaign { Campaign { id: CampaignId::new(), channel: self.channel, diff --git a/sentry/src/routes/campaign.rs b/sentry/src/routes/campaign.rs index 3753b5096..764602b04 100644 --- a/sentry/src/routes/campaign.rs +++ b/sentry/src/routes/campaign.rs @@ -12,7 +12,7 @@ pub async fn create_campaign( let campaign = serde_json::from_slice::(&body) .map_err(|e| ResponseError::FailedValidation(e.to_string()))? // create the actual `Campaign` with random `CampaignId` - .to_campaign(); + .convert_to_campaign(); // TODO AIP#61: Validate Campaign From 13c2c256d75d27ea97c069ebf7bb43642b22c5f2 Mon Sep 17 00:00:00 2001 From: simzzz Date: Mon, 17 May 2021 19:46:40 +0300 Subject: [PATCH 13/23] code clean up --- sentry/src/db/campaign.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index 190216382..773b63b27 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -1,6 +1,5 @@ use crate::db::{DbPool, PoolError}; -use primitives::{channel::ChannelId, AdUnit, Campaign}; -use std::convert::TryFrom; +use primitives::{AdUnit, Campaign}; use tokio_postgres::types::Json; pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result { @@ -35,16 +34,16 @@ pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result Result { let client = pool.get().await?; - let statement = client.prepare("SELECT id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to FROM campaigns WHERE id = $1 AND channel_id = $2").await?; + let statement = client.prepare("SELECT id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to FROM campaigns WHERE id = $1").await?; let row = client .query_one( &statement, - &[&campaign.id, &ChannelId::from(campaign.channel.id())], + &[&campaign.id], ) .await?; From 78695f6993119e469e0c313993486968fa35d1aa Mon Sep 17 00:00:00 2001 From: simzzz Date: Mon, 17 May 2021 20:46:50 +0300 Subject: [PATCH 14/23] fixed failing test --- primitives/src/campaign.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/primitives/src/campaign.rs b/primitives/src/campaign.rs index 1efad5e2a..147763e2c 100644 --- a/primitives/src/campaign.rs +++ b/primitives/src/campaign.rs @@ -393,7 +393,7 @@ mod postgres { title: row.get("title"), pricing_bounds: row.get("pricing_bounds"), event_submission: row.get("event_submission"), - ad_units: row.get("ad_units"), + ad_units: serde_json::from_value(row.get("ad_units")).unwrap(), targeting_rules: row.get("targeting_rules"), created: row.get("created"), active: Active { From 9be0bc5a0dafb6c708e78ec4c689f56c2084fa1b Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 18 May 2021 10:06:58 +0300 Subject: [PATCH 15/23] sentry - db - fix issue with creating test DB pool twice --- sentry/src/db.rs | 23 ++++++++++++----------- sentry/src/db/campaign.rs | 23 ++++++----------------- sentry/src/db/spendable.rs | 17 +++++------------ 3 files changed, 23 insertions(+), 40 deletions(-) diff --git a/sentry/src/db.rs b/sentry/src/db.rs index 0207ecc07..bdf014259 100644 --- a/sentry/src/db.rs +++ b/sentry/src/db.rs @@ -147,26 +147,27 @@ pub mod tests_postgres { use deadpool::managed::{Manager as ManagerTrait, RecycleResult}; use deadpool_postgres::ManagerConfig; + use once_cell::sync::Lazy; use tokio_postgres::{NoTls, SimpleQueryMessage}; use async_trait::async_trait; - use super::{DbPool, PoolError}; + use super::{DbPool, PoolError, POSTGRES_CONFIG}; pub type Pool = deadpool::managed::Pool; - /// we must have a duplication of the migration because of how migrant is handling migratoins - /// we need to separately setup test migrations - pub static MIGRATIONS: &[&str] = &["20190806011140_initial-tables"]; - - pub fn test_postgres_connection(base_config: tokio_postgres::Config) -> Pool { + pub static DATABASE_POOL: Lazy = Lazy::new(|| { let manager_config = ManagerConfig { recycling_method: deadpool_postgres::RecyclingMethod::Fast, }; - let manager = Manager::new(base_config, manager_config); + let manager = Manager::new(POSTGRES_CONFIG.clone(), manager_config); Pool::new(manager, 15) - } + }); + + /// we must have a duplication of the migration because of how migrant is handling migrations + /// we need to separately setup test migrations + pub static MIGRATIONS: &[&str] = &["20190806011140_initial-tables"]; /// A Database is used to isolate test runs from each other /// we need to know the name of the database we've created. @@ -191,9 +192,8 @@ pub mod tests_postgres { } } - /// Base Pool and Config are used to create a new SCHEMA and later on - /// create the actual with default options set for each connection to that SCHEMA - /// Otherwise we cannot create/ + /// Base Pool and Config are used to create a new DATABASE and later on + /// create the actual connection to the database with default options set pub struct Manager { base_config: tokio_postgres::Config, base_pool: deadpool_postgres::Pool, @@ -233,6 +233,7 @@ pub mod tests_postgres { async fn create(&self) -> Result { let pool_index = self.index.fetch_add(1, Ordering::SeqCst); let db_name = format!("test_{}", pool_index); + dbg!(&db_name); // 1. Drop the database if it exists - if a test failed before, the database wouldn't have been removed // 2. Create database diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index 773b63b27..dfeb1258c 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -40,12 +40,7 @@ pub async fn fetch_campaign(pool: DbPool, campaign: &Campaign) -> Result Date: Wed, 19 May 2021 15:50:23 +0300 Subject: [PATCH 16/23] requested changes from review --- primitives/src/campaign.rs | 4 ++-- primitives/src/sentry.rs | 2 +- sentry/src/db/campaign.rs | 4 ++-- sentry/src/db/spendable.rs | 11 ++++------- sentry/src/routes/campaign.rs | 2 +- 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/primitives/src/campaign.rs b/primitives/src/campaign.rs index 147763e2c..f4738ce34 100644 --- a/primitives/src/campaign.rs +++ b/primitives/src/campaign.rs @@ -376,7 +376,7 @@ pub mod validators { #[cfg(feature = "postgres")] mod postgres { - use super::{Active, Campaign, CampaignId, PricingBounds, Validators}; + use super::{Active, AdUnit, Campaign, CampaignId, PricingBounds, Validators}; use bytes::BytesMut; use postgres_types::{accepts, to_sql_checked, FromSql, IsNull, Json, ToSql, Type}; use std::error::Error; @@ -393,7 +393,7 @@ mod postgres { title: row.get("title"), pricing_bounds: row.get("pricing_bounds"), event_submission: row.get("event_submission"), - ad_units: serde_json::from_value(row.get("ad_units")).unwrap(), + ad_units: row.get::<_, Json<_>>("ad_units").0, targeting_rules: row.get("targeting_rules"), created: row.get("created"), active: Active { diff --git a/primitives/src/sentry.rs b/primitives/src/sentry.rs index 2c776dfe7..f70c882a6 100644 --- a/primitives/src/sentry.rs +++ b/primitives/src/sentry.rs @@ -362,7 +362,7 @@ pub mod campaign_create { impl CreateCampaign { /// Creates the new `Campaign` with randomly generated `CampaignId` - pub fn convert_to_campaign(self) -> Campaign { + pub fn into_campaign(self) -> Campaign { Campaign { id: CampaignId::new(), channel: self.channel, diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index dfeb1258c..c4f2b049b 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -4,7 +4,7 @@ use tokio_postgres::types::Json; pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result { let client = pool.get().await?; - let ad_units: Json> = Json(campaign.ad_units.clone()); + let ad_units = Json(campaign.ad_units.clone()); let stmt = client.prepare("INSERT INTO campaigns (id, channel_id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)").await?; let row = client .execute( @@ -33,7 +33,7 @@ pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result Result { diff --git a/sentry/src/db/spendable.rs b/sentry/src/db/spendable.rs index 0080a4983..2d9eeaee7 100644 --- a/sentry/src/db/spendable.rs +++ b/sentry/src/db/spendable.rs @@ -80,13 +80,10 @@ mod test { assert!(is_inserted); - let fetched_spendable = fetch_spendable( - db_pool.clone(), - &spendable.spender, - &spendable.channel.id(), - ) - .await - .expect("Should fetch successfully"); + let fetched_spendable = + fetch_spendable(db_pool.clone(), &spendable.spender, &spendable.channel.id()) + .await + .expect("Should fetch successfully"); assert_eq!(spendable, fetched_spendable); } diff --git a/sentry/src/routes/campaign.rs b/sentry/src/routes/campaign.rs index 764602b04..94076e34b 100644 --- a/sentry/src/routes/campaign.rs +++ b/sentry/src/routes/campaign.rs @@ -12,7 +12,7 @@ pub async fn create_campaign( let campaign = serde_json::from_slice::(&body) .map_err(|e| ResponseError::FailedValidation(e.to_string()))? // create the actual `Campaign` with random `CampaignId` - .convert_to_campaign(); + .into_campaign(); // TODO AIP#61: Validate Campaign From a5719e065f9fb64f584a8483ae768e59ffc398fc Mon Sep 17 00:00:00 2001 From: simzzz Date: Thu, 20 May 2021 11:37:56 +0300 Subject: [PATCH 17/23] removed dbg --- sentry/src/db.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry/src/db.rs b/sentry/src/db.rs index bdf014259..6dd6b6639 100644 --- a/sentry/src/db.rs +++ b/sentry/src/db.rs @@ -233,7 +233,6 @@ pub mod tests_postgres { async fn create(&self) -> Result { let pool_index = self.index.fetch_add(1, Ordering::SeqCst); let db_name = format!("test_{}", pool_index); - dbg!(&db_name); // 1. Drop the database if it exists - if a test failed before, the database wouldn't have been removed // 2. Create database From 869a141dc964308f627b2177392d861325856fd9 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 20 May 2021 13:22:19 +0300 Subject: [PATCH 18/23] sentry - db - campaing- fix warnings --- sentry/src/db/campaign.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/sentry/src/db/campaign.rs b/sentry/src/db/campaign.rs index c4f2b049b..c3660d68e 100644 --- a/sentry/src/db/campaign.rs +++ b/sentry/src/db/campaign.rs @@ -1,7 +1,9 @@ use crate::db::{DbPool, PoolError}; -use primitives::{AdUnit, Campaign}; +use primitives::Campaign; use tokio_postgres::types::Json; +// TODO: Remove once we use this fn +#[allow(dead_code)] pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result { let client = pool.get().await?; let ad_units = Json(campaign.ad_units.clone()); @@ -36,6 +38,8 @@ pub async fn insert_campaign(pool: &DbPool, campaign: &Campaign) -> Result Result { let client = pool.get().await?; let statement = client.prepare("SELECT id, channel, creator, budget, validators, title, pricing_bounds, event_submission, ad_units, targeting_rules, created, active_from, active_to FROM campaigns WHERE id = $1").await?; From dce813988e766d1dcf8134ce5a58512aa3216c7a Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 20 May 2021 13:26:15 +0300 Subject: [PATCH 19/23] primitives - config - add min_validator_fee - remove unused Config values - update `docs/config/{dev/prod}.toml` --- docs/config/dev.toml | 21 +++++++++++++++------ docs/config/prod.toml | 28 ++++++++++++++++++++-------- primitives/src/config.rs | 15 +++++++-------- 3 files changed, 42 insertions(+), 22 deletions(-) diff --git a/docs/config/dev.toml b/docs/config/dev.toml index 3ec9f9a98..04632adc9 100644 --- a/docs/config/dev.toml +++ b/docs/config/dev.toml @@ -29,21 +29,30 @@ ethereum_network = 'http://localhost:8545' ethereum_adapter_relayer = 'https://goerli-relayer.adex.network' creators_whitelist = [] -minimal_deposit = "0" -minimal_fee = "0" validators_whitelist = [] [[token_address_whitelist]] -address = '0x73967c6a0904aa032c103b4104747e88c566b1a2' #DAI +# DAI +address = '0x73967c6a0904aa032c103b4104747e88c566b1a2' +# 1 * 10^-10 = 0.0_000_000_001 min_token_units_for_deposit = '100000000' +min_validator_fee = '100000000' precision = 18 [[token_address_whitelist]] -address = '0x509ee0d083ddf8ac028f2a56731412edd63223b9' #USDT -min_token_units_for_deposit = '100000000' +# USDT +address = '0x509ee0d083ddf8ac028f2a56731412edd63223b9' +# 1.000_000 +min_token_units_for_deposit = '1000000' +# 0.001 +min_validator_fee = '1000' precision = 6 [[token_address_whitelist]] -address = '0x44dcfcead37be45206af6079648988b29284b2c6' #USDC +# USDC +address = '0x44dcfcead37be45206af6079648988b29284b2c6' +# 1.000_000 min_token_units_for_deposit = '100000000' +# 0.001 +min_validator_fee = '1000' precision = 6 diff --git a/docs/config/prod.toml b/docs/config/prod.toml index 57e7b8fcb..50b68c48e 100644 --- a/docs/config/prod.toml +++ b/docs/config/prod.toml @@ -28,27 +28,39 @@ ethereum_network = 'http://localhost:8545' ethereum_adapter_relayer = 'https://relayer.adex.network' creators_whitelist = [] -minimal_deposit = "0" -minimal_fee = "0" validators_whitelist = [] + [[token_address_whitelist]] -address = '0x6b175474e89094c44da98b954eedeac495271d0f' #DAI +# DAI +address = '0x6b175474e89094c44da98b954eedeac495271d0f' +# 1 * 10^-10 = 0.0_000_000_001 min_token_units_for_deposit = '100000000' +min_validator_fee = '100000000' precision = 18 [[token_address_whitelist]] -address = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359' #SAI +# SAI +address = '0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359' +# 1 * 10^-10 = 0.0_000_000_001 min_token_units_for_deposit = '100000000' +min_validator_fee = '100000000' precision = 18 [[token_address_whitelist]] -address = '0xdac17f958d2ee523a2206206994597c13d831ec7' #USDT -min_token_units_for_deposit = '100000000' +# USDT +address = '0xdac17f958d2ee523a2206206994597c13d831ec7' +# 1.000_000 +min_token_units_for_deposit = '1000000' +# 0.001 +min_validator_fee = '1000' precision = 6 [[token_address_whitelist]] -address = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' #USDC +# USDC +address = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48' +# 1.000_000 min_token_units_for_deposit = '100000000' +# 0.001 +min_validator_fee = '1000' precision = 6 - diff --git a/primitives/src/config.rs b/primitives/src/config.rs index eb42ffb06..5437398a5 100644 --- a/primitives/src/config.rs +++ b/primitives/src/config.rs @@ -18,6 +18,7 @@ lazy_static! { #[derive(Serialize, Deserialize, Debug, Clone)] pub struct TokenInfo { pub min_token_units_for_deposit: BigNum, + pub min_validator_fee: BigNum, pub precision: NonZeroU8, } @@ -43,10 +44,6 @@ pub struct Config { pub minimal_fee: BigNum, #[serde(deserialize_with = "deserialize_token_whitelist")] pub token_address_whitelist: HashMap, - /// DEPRECATED since this is v4! - #[deprecated(note = "REMOVE, this is V4")] - #[serde(with = "SerHex::")] - pub ethereum_core_address: [u8; 20], #[serde(with = "SerHex::")] pub outpace_address: [u8; 20], #[serde(with = "SerHex::")] @@ -60,6 +57,7 @@ pub struct Config { struct ConfigWhitelist { address: Address, min_token_units_for_deposit: BigNum, + min_validator_fee: BigNum, precision: NonZeroU8, } @@ -73,12 +71,13 @@ where let tokens_whitelist: HashMap = array .into_iter() - .map(|i| { + .map(|config_whitelist| { ( - i.address, + config_whitelist.address, TokenInfo { - min_token_units_for_deposit: i.min_token_units_for_deposit, - precision: i.precision, + min_token_units_for_deposit: config_whitelist.min_token_units_for_deposit, + min_validator_fee: config_whitelist.min_validator_fee, + precision: config_whitelist.precision, }, ) }) From 33a6924c6c783048c00f7b735168744f1f99282e Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 20 May 2021 13:26:28 +0300 Subject: [PATCH 20/23] fix rustfmt & clippy --- primitives/src/adapter.rs | 3 +- primitives/src/campaign.rs | 3 +- primitives/src/campaign_validator.rs | 75 ++++++++++++++++------------ 3 files changed, 45 insertions(+), 36 deletions(-) diff --git a/primitives/src/adapter.rs b/primitives/src/adapter.rs index f65f1a3a7..af336daff 100644 --- a/primitives/src/adapter.rs +++ b/primitives/src/adapter.rs @@ -1,6 +1,5 @@ use crate::{ - channel::ChannelError, channel_v5::Channel, Address, - BigNum, DomainError, ValidatorId, + channel::ChannelError, channel_v5::Channel, Address, BigNum, DomainError, ValidatorId, }; use async_trait::async_trait; use serde::{Deserialize, Serialize}; diff --git a/primitives/src/campaign.rs b/primitives/src/campaign.rs index f4738ce34..5774ec478 100644 --- a/primitives/src/campaign.rs +++ b/primitives/src/campaign.rs @@ -375,8 +375,7 @@ pub mod validators { #[cfg(feature = "postgres")] mod postgres { - - use super::{Active, AdUnit, Campaign, CampaignId, PricingBounds, Validators}; + use super::{Active, Campaign, CampaignId, PricingBounds, Validators}; use bytes::BytesMut; use postgres_types::{accepts, to_sql_checked, FromSql, IsNull, Json, ToSql, Type}; use std::error::Error; diff --git a/primitives/src/campaign_validator.rs b/primitives/src/campaign_validator.rs index d1e1c460a..657b12040 100644 --- a/primitives/src/campaign_validator.rs +++ b/primitives/src/campaign_validator.rs @@ -1,19 +1,18 @@ -use crate::{campaign::Validators, config::Config, Address, Campaign, UnifiedNum, ValidatorId}; +use crate::{ + campaign::Validators, + config::{Config, TokenInfo}, + Address, Campaign, UnifiedNum, ValidatorId, +}; use chrono::Utc; -use std::cmp::PartialEq; +use std::{cmp::PartialEq, collections::HashMap}; use thiserror::Error; pub trait Validator { - fn validate( - &self, - config: &Config, - validator_identity: &ValidatorId, - ) -> Result; + fn validate(&self, config: &Config, validator_identity: &ValidatorId) -> Result<(), Error>; } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum Validation { - Ok, /// When the Adapter address is not listed in the `campaign.validators` & `campaign.channel.(leader/follower)` /// which in terms means, that the adapter shouldn't handle this Campaign AdapterNotIncluded, @@ -32,44 +31,56 @@ pub enum Validation { pub enum Error { #[error("Summing the Validators fee results in overflow")] FeeSumOverflow, + #[error("Validation error: {0:?}")] + Validation(Validation), +} + +impl From for Error { + fn from(v: Validation) -> Self { + Self::Validation(v) + } } impl Validator for Campaign { - fn validate( - &self, - config: &Config, - validator_identity: &ValidatorId, - ) -> Result { + fn validate(&self, config: &Config, validator_identity: &ValidatorId) -> Result<(), Error> { // check if the channel validators include our adapter identity let whoami_validator = match self.find_validator(validator_identity) { Some(role) => role.validator(), - None => return Ok(Validation::AdapterNotIncluded), + None => return Err(Validation::AdapterNotIncluded.into()), }; if self.active.to < Utc::now() { - return Ok(Validation::InvalidActiveTo); + return Err(Validation::InvalidActiveTo.into()); } if !all_validators_listed(&self.validators, &config.validators_whitelist) { - return Ok(Validation::UnlistedValidator); + return Err(Validation::UnlistedValidator.into()); } if !creator_listed(&self, &config.creators_whitelist) { - return Ok(Validation::UnlistedCreator); + return Err(Validation::UnlistedCreator.into()); } - if !asset_listed(&self, &config.token_address_whitelist) { - return Ok(Validation::UnlistedAsset); + // Check if the token is listed in the Configuration + let token_info = config + .token_address_whitelist + .get(&self.channel.token) + .ok_or(Validation::UnlistedAsset)?; + + // Check if the campaign budget is above the minimum deposit configured + if self.budget.to_precision(token_info.precision.get()) + < token_info.min_token_units_for_deposit + { + return Err(Validation::MinimumDepositNotMet.into()); } - // TODO AIP#61: Use configuration to check the minimum deposit of the token! - if self.budget < UnifiedNum::from(500) { - return Ok(Validation::MinimumDepositNotMet); - } - - // TODO AIP#61: Use Configuration to check the minimum validator fee of the token! - if whoami_validator.fee < UnifiedNum::from(100) { - return Ok(Validation::MinimumValidatorFeeNotMet); + // Check if the validator fee is greater than the minimum configured fee + if whoami_validator + .fee + .to_precision(token_info.precision.get()) + < token_info.min_validator_fee + { + return Err(Validation::MinimumValidatorFeeNotMet.into()); } let total_validator_fee: UnifiedNum = self @@ -81,10 +92,10 @@ impl Validator for Campaign { .ok_or(Error::FeeSumOverflow)?; if total_validator_fee >= self.budget { - return Ok(Validation::FeeConstraintViolated); + return Err(Validation::FeeConstraintViolated.into()); } - Ok(Validation::Ok) + Ok(()) } } @@ -112,11 +123,11 @@ pub fn creator_listed(campaign: &Campaign, whitelist: &[Address]) -> bool { .any(|allowed| allowed.eq(&campaign.creator)) } -pub fn asset_listed(campaign: &Campaign, whitelist: &[String]) -> bool { +pub fn asset_listed(campaign: &Campaign, whitelist: &HashMap) -> bool { // if the list is empty, return true, as we don't have a whitelist to restrict us to // or if we have a list, check if it includes the `channel.deposit_asset` whitelist.is_empty() || whitelist - .iter() - .any(|allowed| allowed == &campaign.channel.token.to_string()) + .keys() + .any(|allowed| allowed == &campaign.channel.token) } From 78dcc7f30c4db6115c4cc50e9fb58d42bd47cd1a Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 20 May 2021 13:40:53 +0300 Subject: [PATCH 21/23] primitives - remove ChannelValidator --- primitives/src/channel_validator.rs | 118 ---------------------------- primitives/src/lib.rs | 1 - 2 files changed, 119 deletions(-) delete mode 100644 primitives/src/channel_validator.rs diff --git a/primitives/src/channel_validator.rs b/primitives/src/channel_validator.rs deleted file mode 100644 index 7b36fc43d..000000000 --- a/primitives/src/channel_validator.rs +++ /dev/null @@ -1,118 +0,0 @@ -use crate::channel::{Channel, ChannelError, SpecValidator, SpecValidators}; -use crate::config::{Config, TokenInfo}; -use crate::Address; -use crate::BigNum; -use crate::ValidatorId; -use chrono::Utc; -use std::cmp::PartialEq; -use std::collections::HashMap; -use time::Duration; - -// -// TODO: AIP#61 OBSOLETE, remove once we remove the Channel Create!How relevant is this validator? Check and remove if it's obsolete -// -pub trait ChannelValidator { - fn is_channel_valid( - config: &Config, - validator_identity: &ValidatorId, - channel: &Channel, - ) -> Result<(), ChannelError> { - let adapter_channel_validator = match channel.spec.validators.find(validator_identity) { - // check if the channel validators include our adapter identity - None => return Err(ChannelError::AdapterNotIncluded), - Some(SpecValidator::Leader(validator)) | Some(SpecValidator::Follower(validator)) => { - validator - } - }; - - if channel.valid_until < Utc::now() { - return Err(ChannelError::InvalidValidUntil( - "channel.validUntil has passed".to_string(), - )); - } - - if channel.valid_until > (Utc::now() + Duration::days(365)) { - return Err(ChannelError::InvalidValidUntil( - "channel.validUntil should not be greater than one year".to_string(), - )); - } - - if channel.spec.withdraw_period_start > channel.valid_until { - return Err(ChannelError::InvalidValidUntil( - "channel withdrawPeriodStart is invalid".to_string(), - )); - } - - if !all_validators_listed(&channel.spec.validators, &config.validators_whitelist) { - return Err(ChannelError::UnlistedValidator); - } - - if !creator_listed( - &channel, - &config - .creators_whitelist - .iter() - .map(Into::into) - .collect::>(), - ) { - return Err(ChannelError::UnlistedCreator); - } - - if !asset_listed(&channel, &config.token_address_whitelist) { - return Err(ChannelError::UnlistedAsset); - } - - if channel.deposit_amount < config.minimal_deposit { - return Err(ChannelError::MinimumDepositNotMet); - } - - if BigNum::from(adapter_channel_validator.fee.to_u64()) < config.minimal_fee { - return Err(ChannelError::MinimumValidatorFeeNotMet); - } - - let total_validator_fee: BigNum = channel - .spec - .validators - .iter() - .map(|v| BigNum::from(v.fee.to_u64())) - .fold(BigNum::from(0), |acc, x| acc + x); - - if total_validator_fee >= channel.deposit_amount { - return Err(ChannelError::FeeConstraintViolated); - } - - Ok(()) - } -} - -pub fn all_validators_listed(validators: &SpecValidators, whitelist: &[ValidatorId]) -> bool { - if whitelist.is_empty() { - true - } else { - let found_validators = whitelist - .iter() - .filter(|&allowed| { - allowed == &validators.leader().id || allowed == &validators.follower().id - }) - // this will ensure that if we find the 2 validators earlier - // we don't go over the other values of the whitelist - .take(2); - // the found validators should be exactly 2, if they are not, then 1 or 2 are missing - found_validators.count() == 2 - } -} - -pub fn creator_listed(channel: &Channel, whitelist: &[ValidatorId]) -> bool { - // if the list is empty, return true, as we don't have a whitelist to restrict us to - // or if we have a list, check if it includes the `channel.creator` - whitelist.is_empty() || whitelist.iter().any(|allowed| allowed.eq(&channel.creator)) -} - -pub fn asset_listed(channel: &Channel, whitelist: &HashMap) -> bool { - // if the list is empty, return true, as we don't have a whitelist to restrict us to - // or if we have a list, check if it includes the `channel.deposit_asset` - whitelist.is_empty() - || whitelist - .keys() - .any(|allowed| allowed.to_string() == channel.deposit_asset) -} diff --git a/primitives/src/lib.rs b/primitives/src/lib.rs index 8089710f0..0ede0b218 100644 --- a/primitives/src/lib.rs +++ b/primitives/src/lib.rs @@ -28,7 +28,6 @@ pub mod campaign; pub mod campaign_validator; pub mod channel; pub mod channel_v5; -pub mod channel_validator; pub mod config; mod eth_checksum; pub mod event_submission; From 7ae6e566a5699185e380865c22dd7048167e0950 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 20 May 2021 13:41:09 +0300 Subject: [PATCH 22/23] adapter - ethereum - fix test_utils --- adapter/src/ethereum/test_utils.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/adapter/src/ethereum/test_utils.rs b/adapter/src/ethereum/test_utils.rs index 0dbb4b5fb..da2e40c57 100644 --- a/adapter/src/ethereum/test_utils.rs +++ b/adapter/src/ethereum/test_utils.rs @@ -234,6 +234,8 @@ pub async fn deploy_token_contract( let token_info = TokenInfo { min_token_units_for_deposit: BigNum::from(min_token_units), precision: NonZeroU8::new(18).expect("should create NonZeroU8"), + // 0.000_1 + min_validator_fee: BigNum::from(100_000_000_000_000), }; Ok((token_info, token_contract.address(), token_contract)) From 21037aaa51e02e164ff93aeba881829aaa2f3757 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 20 May 2021 13:41:29 +0300 Subject: [PATCH 23/23] primitives - config - clean-up old unused values --- docs/config/dev.toml | 1 - docs/config/prod.toml | 2 +- primitives/src/config.rs | 12 +++++------- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/docs/config/dev.toml b/docs/config/dev.toml index 04632adc9..6ad28e737 100644 --- a/docs/config/dev.toml +++ b/docs/config/dev.toml @@ -19,7 +19,6 @@ validator_tick_timeout = 5000 ip_rate_limit = { type = 'ip', timeframe = 20000 } sid_rate_limit = { type = 'sid', timeframe = 20000 } -ethereum_core_address = '0x333420fc6a897356e69b62417cd17ff012177d2b' # TODO: Replace with real contract address outpace_address = '0x333420fc6a897356e69b62417cd17ff012177d2b' # TODO: Replace with real contract address diff --git a/docs/config/prod.toml b/docs/config/prod.toml index 50b68c48e..97371f420 100644 --- a/docs/config/prod.toml +++ b/docs/config/prod.toml @@ -18,7 +18,7 @@ validator_tick_timeout = 10000 ip_rate_limit = { type = 'ip', timeframe = 1200000 } sid_rate_limit = { type = 'sid', timeframe = 0 } -ethereum_core_address = '0x333420fc6a897356e69b62417cd17ff012177d2b' + # TODO: Replace with real contract address outpace_address = '0x333420fc6a897356e69b62417cd17ff012177d2b' # TODO: Replace with real contract address diff --git a/primitives/src/config.rs b/primitives/src/config.rs index 5437398a5..784362005 100644 --- a/primitives/src/config.rs +++ b/primitives/src/config.rs @@ -26,12 +26,12 @@ pub struct TokenInfo { #[serde(rename_all(serialize = "SCREAMING_SNAKE_CASE"))] pub struct Config { pub max_channels: u32, + pub channels_find_limit: u32, pub wait_time: u32, pub aggr_throttle: u32, - pub heartbeat_time: u32, // in milliseconds - pub channels_find_limit: u32, pub events_find_limit: u32, pub msgs_find_limit: u32, + pub heartbeat_time: u32, // in milliseconds pub health_threshold_promilles: u32, pub health_unsignable_promilles: u32, pub propagation_timeout: u32, @@ -39,18 +39,16 @@ pub struct Config { pub validator_tick_timeout: u32, pub ip_rate_limit: RateLimit, // HashMap?? pub sid_rate_limit: RateLimit, // HashMap ?? - pub creators_whitelist: Vec
, - pub minimal_deposit: BigNum, - pub minimal_fee: BigNum, - #[serde(deserialize_with = "deserialize_token_whitelist")] - pub token_address_whitelist: HashMap, #[serde(with = "SerHex::")] pub outpace_address: [u8; 20], #[serde(with = "SerHex::")] pub sweeper_address: [u8; 20], pub ethereum_network: String, pub ethereum_adapter_relayer: String, + pub creators_whitelist: Vec
, pub validators_whitelist: Vec, + #[serde(deserialize_with = "deserialize_token_whitelist")] + pub token_address_whitelist: HashMap, } #[derive(Serialize, Deserialize, Debug, Clone)]