From 72b62addb29cc3e528c39bebecc3a6ea9fe94ca0 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 19 Oct 2022 18:05:47 +0300 Subject: [PATCH 01/15] adview-manager - serve - Cargo add deps --- Cargo.lock | 141 +++++++++++++++++++++++++++----- adview-manager/serve/Cargo.toml | 14 +++- 2 files changed, 130 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9ac47ebe4..28590c288 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,14 +64,16 @@ name = "adview-serve" version = "0.1.0" dependencies = [ "adview-manager", + "anyhow", "axum", "chrono", - "env_logger", - "log", + "pretty_assertions", "primitives", "serde", "tera", "tokio", + "tracing", + "tracing-subscriber", "wiremock", ] @@ -136,6 +138,15 @@ dependencies = [ "libc", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + [[package]] name = "anyhow" version = "1.0.65" @@ -328,6 +339,7 @@ checksum = "c9e3356844c4d6a6d6467b8da2cffb4a2820be256f50a3a386c9d152bab31043" dependencies = [ "async-trait", "axum-core", + "axum-macros", "bitflags", "bytes", "futures-util", @@ -368,6 +380,18 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6293dae2ec708e679da6736e857cf8532886ef258e92930f38279c12641628b8" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "axum-server" version = "0.4.2" @@ -1098,19 +1122,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "env_logger" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c90bf5f19754d10198ccb95b70664fc925bd1fc090a0fd9a6ebc54acc8cd6272" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "envy" version = "0.4.2" @@ -1539,6 +1550,12 @@ dependencies = [ "http", ] +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + [[package]] name = "hermit-abi" version = "0.1.19" @@ -1640,12 +1657,6 @@ version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02296996cb8796d7c6e3bc2d9211b7802812d36999a51bb754123ead7d37d026" -[[package]] -name = "humantime" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" - [[package]] name = "hyper" version = "0.14.20" @@ -1927,6 +1938,15 @@ dependencies = [ "value-bag", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.9" @@ -2988,6 +3008,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.27" @@ -3496,6 +3525,15 @@ dependencies = [ "keccak", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -4055,9 +4093,21 @@ dependencies = [ "cfg-if", "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tracing-core" version = "0.1.30" @@ -4065,6 +4115,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +dependencies = [ + "ansi_term", + "matchers", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec 1.10.0", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", ] [[package]] @@ -4253,6 +4346,12 @@ dependencies = [ "wiremock", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "value-bag" version = "1.0.0-alpha.9" diff --git a/adview-manager/serve/Cargo.toml b/adview-manager/serve/Cargo.toml index 2f7f0c7fd..646b68608 100644 --- a/adview-manager/serve/Cargo.toml +++ b/adview-manager/serve/Cargo.toml @@ -12,13 +12,17 @@ publish = false # Domain adex_primitives = { version = "0.2.0", path = "../../primitives", package = "primitives", features = ["test-util"] } adview-manager = { path = "../" } + +# Application errors +anyhow = "1" +# Time chrono = "0.4" # Async runtime tokio = { version = "1", features = ["macros", "time", "rt-multi-thread"] } # Web Server -axum = "0.5" +axum = {version = "0.5", features = ["headers", "macros"]} # Template engine tera = { version = "1" } @@ -29,6 +33,8 @@ wiremock = { version = "0.5" } # (De)Serialization serde = { version = "^1.0", features = ["derive"] } -# Logging -log = "0.4" -env_logger = { version = "0.9" } +# Tracing +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +pretty_assertions = "1" From 986ec73ee7894487bae08ac050921f16db1bdf29 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 19 Oct 2022 18:06:20 +0300 Subject: [PATCH 02/15] adview serve - add tracing subscriber --- adview-manager/serve/src/main.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/adview-manager/serve/src/main.rs b/adview-manager/serve/src/main.rs index 8e779e022..bd03fd46d 100644 --- a/adview-manager/serve/src/main.rs +++ b/adview-manager/serve/src/main.rs @@ -2,7 +2,9 @@ use adview_serve::app::Application; #[tokio::main] async fn main() -> Result<(), Box> { - env_logger::init(); + let tracing_subscriber = tracing_subscriber::FmtSubscriber::new(); + tracing::subscriber::set_global_default(tracing_subscriber) + .expect("setting tracing default failed"); Application::new()?.run().await?; From b7777c336ee18b3d4025d2f194d25e6836f409d4 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 19 Oct 2022 18:06:43 +0300 Subject: [PATCH 03/15] primitives - AdSlot - add default for modify --- primitives/src/ad_slot.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/primitives/src/ad_slot.rs b/primitives/src/ad_slot.rs index bd569e965..68574fdec 100644 --- a/primitives/src/ad_slot.rs +++ b/primitives/src/ad_slot.rs @@ -54,6 +54,6 @@ pub struct AdSlot { #[serde(default)] pub archived: bool, /// UTC timestamp in milliseconds, changed every time modifiable property is changed - #[serde(with = "ts_milliseconds_option")] + #[serde(default, with = "ts_milliseconds_option")] pub modified: Option>, } From c5c75716b30496b136d54716cd4d712f3b1b9af8 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Wed, 19 Oct 2022 18:28:50 +0300 Subject: [PATCH 04/15] adview serve: - new preview route - improved Manager for random position generation --- adview-manager/README.md | 7 +- adview-manager/serve/src/app.rs | 79 ++++- adview-manager/serve/src/main.rs | 2 +- adview-manager/serve/src/routes.rs | 312 ++++++++++++++++++-- adview-manager/serve/templates/base.html | 9 + adview-manager/serve/templates/next_ad.html | 29 ++ adview-manager/src/helpers.rs | 2 +- adview-manager/src/manager.rs | 26 +- primitives/examples/ad_slot.rs | 66 +++++ 9 files changed, 496 insertions(+), 36 deletions(-) create mode 100644 adview-manager/serve/templates/next_ad.html create mode 100644 primitives/examples/ad_slot.rs diff --git a/adview-manager/README.md b/adview-manager/README.md index fe6f385e1..516bb8d05 100644 --- a/adview-manager/README.md +++ b/adview-manager/README.md @@ -12,7 +12,7 @@ a publisher websites. Running the local server: -`RUST_LOG=debug cargo run -p serve` +`RUST_LOG=debug cargo run -p adview-serve` This will start a server at `127.0.0.1:3030` @@ -21,4 +21,7 @@ for the server. Routes: -- `GET /ad` - visualizes a single ad \ No newline at end of file +- `GET /` - landing page +- `GET /preview/ad` - preview a single example ad +- `GET /preview/video` - preview a single example ad +- `POST /:slot` - preview a single example ad \ No newline at end of file diff --git a/adview-manager/serve/src/app.rs b/adview-manager/serve/src/app.rs index 50c239006..e1503af44 100644 --- a/adview-manager/serve/src/app.rs +++ b/adview-manager/serve/src/app.rs @@ -1,11 +1,16 @@ use std::{net::SocketAddr, sync::Arc}; -use axum::{routing::get, Extension, Router, Server}; -use log::info; +use axum::{ + http::StatusCode, + response::IntoResponse, + routing::{get, post}, + Extension, Router, Server, +}; +use tracing::{error, info}; use tera::Tera; -use crate::routes::{get_index, get_preview_ad, get_preview_video}; +use crate::routes::{get_index, get_preview_ad, get_preview_video, post_slot_preview}; #[derive(Debug)] pub struct State { @@ -17,6 +22,66 @@ pub struct Application { state: Arc, } +pub struct Error { + error: Box, + status: StatusCode, +} + +impl Error { + pub fn new(error: E, status: StatusCode) -> Self + where + E: Into>, + { + Self { + error: error.into(), + status, + } + } + + /// Create a new [`Error`] from [`anyhow::Error`] with a custom [`StatusCode`] + /// instead of the default [`StatusCode::INTERNAL_SERVER_ERROR`]. + pub fn anyhow_status(error: anyhow::Error, status: StatusCode) -> Self { + Self { + error: error.into(), + status, + } + } +} + +impl IntoResponse for Error { + fn into_response(self) -> axum::response::Response { + let response_tuple = match self.status { + StatusCode::INTERNAL_SERVER_ERROR => { + error!({error = %self.error}, "Server error"); + (StatusCode::INTERNAL_SERVER_ERROR, self.error.to_string()) + } + // we want to log any error that is with status > 500 + status_code if status_code.as_u16() > 500 => { + error!({error = %self.error}, "Server error"); + (status_code, self.error.to_string()) + } + // anything else is < 500, so it's safe to not log it due to e.g. bad user input + status_code => (status_code, self.error.to_string()), + }; + + response_tuple.into_response() + } +} + +impl From for Error +where + E: Into, +{ + fn from(err: E) -> Self { + let anyhow_err: anyhow::Error = err.into(); + + Self { + error: anyhow_err.into(), + status: StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + impl Application { pub fn new() -> Result> { let serve_dir = match std::env::current_dir().unwrap() { @@ -42,11 +107,15 @@ impl Application { } pub async fn run(&self) -> Result<(), Box> { + let preview_routes = Router::new() + .route("/", post(post_slot_preview)) + .route("/ad", get(get_preview_ad)) + .route("/video", get(get_preview_video)); + // build our application with a single route let app = Router::new() .route("/", get(get_index)) - .route("/preview/ad", get(get_preview_ad)) - .route("/preview/video", get(get_preview_video)) + .nest("/preview", preview_routes) .layer(Extension(self.state.clone())); let socket_addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); diff --git a/adview-manager/serve/src/main.rs b/adview-manager/serve/src/main.rs index bd03fd46d..42c68bd9f 100644 --- a/adview-manager/serve/src/main.rs +++ b/adview-manager/serve/src/main.rs @@ -4,7 +4,7 @@ use adview_serve::app::Application; async fn main() -> Result<(), Box> { let tracing_subscriber = tracing_subscriber::FmtSubscriber::new(); tracing::subscriber::set_global_default(tracing_subscriber) - .expect("setting tracing default failed"); + .expect("setting tracing default failed"); Application::new()?.run().await?; diff --git a/adview-manager/serve/src/routes.rs b/adview-manager/serve/src/routes.rs index 1c3891d98..7646f46aa 100644 --- a/adview-manager/serve/src/routes.rs +++ b/adview-manager/serve/src/routes.rs @@ -1,26 +1,34 @@ use std::sync::Arc; +use anyhow::{anyhow, bail}; +use axum::{ + http::{header::ACCEPT_LANGUAGE, HeaderMap, StatusCode}, + response::Html, + Extension, Json, +}; +use chrono::Utc; +use tera::Context; +use tracing::debug; +use wiremock::{ + matchers::{method, path, query_param}, + Mock, MockServer, ResponseTemplate, +}; + use adex_primitives::{ + config::GANACHE_CONFIG, sentry::{units_for_slot, IMPRESSION}, targeting::{input::Global, Input}, - test_util::{DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, DUMMY_VALIDATOR_LEADER}, + test_util::{ + DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, DUMMY_VALIDATOR_LEADER, PUBLISHER, + }, util::ApiUrl, - ToHex, + AdSlot, ToHex, }; use adview_manager::{ get_unit_html_with_events, manager::Size, manager::DEFAULT_TOKENS, Manager, Options, }; -use axum::{response::Html, Extension}; -use chrono::Utc; -use log::debug; -use wiremock::{ - matchers::{method, path, query_param}, - Mock, MockServer, ResponseTemplate, -}; - -use tera::Context; -use crate::app::State; +use crate::app::{Error, State}; /// `GET /` pub async fn get_index(Extension(state): Extension>) -> Html { @@ -37,7 +45,6 @@ pub async fn get_preview_ad(Extension(state): Extension>) -> Html>) -> Html>) -> Html>) -> Html>) -> Html< .unwrap(); let options = Options { - market_url: "http://placeholder.com".parse().unwrap(), market_slot: DUMMY_IPFS[0], publisher_addr, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared @@ -188,7 +193,6 @@ pub async fn get_preview_video(Extension(state): Extension>) -> Html< false, ); - // let video_html = get_unit_html_with_events(&options, ); let html = { let mut context = Context::new(); context.insert("ad_code", &video_html); @@ -201,3 +205,275 @@ pub async fn get_preview_video(Extension(state): Extension>) -> Html< Html(html) } + +/// `GET /preview/:slot` +// pub async fn get_slot_preview( +// Extension(state): Extension>, +// Path(slot): Path, +// headers: HeaderMap, +// ) -> Result, Error> { +// let config = GANACHE_CONFIG.clone(); + +// // extracted from Accept-language header +// let navigator_language = headers +// .get(ACCEPT_LANGUAGE) +// .map(|value| value.to_str()) +// .transpose()? +// .map(|s| parse_navigator_language(s)) +// .transpose()? +// .flatten() +// // TODO: make configurable? +// .unwrap_or("en".into()); + +// let whitelisted_tokens = config +// .chains +// .iter() +// .map(|(_, chain)| chain.tokens.iter().map(|(_, token)| token.address)) +// .flatten() +// .collect(); + +// let options = Options { +// market_slot: ad_slot.ipfs, +// publisher_addr: *PUBLISHER, +// // All passed tokens must be of the same price, so that the amounts can be accurately compared +// whitelisted_tokens, +// size: Some( +// size_from_type(&ad_slot.ad_type) +// .map_err(|error| Error::anyhow_status(error, StatusCode::BAD_REQUEST))?, +// ), +// navigator_language: Some(navigator_language), +// /// Defaulted +// disabled_video: false, +// disabled_sticky: false, +// }; + +// let manager = Manager::new(options, Default::default())?; + +// let next_ad_unit = manager.get_next_ad_unit().await?; + +// let html = { +// let mut context = Context::new(); +// context.insert("next_ad_unit", &next_ad_unit); +// context.insert("ad_slot", &ad_slot); + +// state +// .tera +// .render("next_ad.html", &context) +// .expect("Should render") +// }; + +// Ok(Html(html)) +// } + +/// `POST /preview` +/// +/// Uses the provided with the POST data [`AdSlot`] and get's a matching [`AdUnit`] html +/// with the manager. +/// +/// Uses the Ganache config to select all the whitelisted tokens in all chains. +/// It's configured to use locally running sentry validators at ports `8005` and `8006`. +#[axum::debug_handler] +pub async fn post_slot_preview( + Extension(state): Extension>, + Json(ad_slot): Json, + headers: HeaderMap, +) -> Result, Error> { + let config = GANACHE_CONFIG.clone(); + + // extracted from Accept-language header + let navigator_language = headers + .get(ACCEPT_LANGUAGE) + .map(|value| value.to_str()) + .transpose()? + .map(parse_navigator_language) + .transpose()? + .flatten() + // TODO: make configurable? + .unwrap_or_else(|| "en".into()); + + let whitelisted_tokens = config + .chains + .iter() + .flat_map(|(_, chain)| chain.tokens.iter().map(|(_, token)| token.address)) + .collect(); + + let options = Options { + market_slot: ad_slot.ipfs, + publisher_addr: *PUBLISHER, + // All passed tokens must be of the same price, so that the amounts can be accurately compared + whitelisted_tokens, + size: Some( + size_from_type(&ad_slot.ad_type) + .map_err(|error| Error::anyhow_status(error, StatusCode::BAD_REQUEST))?, + ), + navigator_language: Some(navigator_language), + /// Defaulted + disabled_video: false, + disabled_sticky: false, + validators: vec![ + ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"), + ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"), + ], + }; + + let manager = Manager::new(options, Default::default())?; + + let next_ad_unit = manager.get_next_ad_unit().await?; + + let html = { + let mut context = Context::new(); + context.insert("next_ad_unit", &next_ad_unit); + context.insert("ad_slot", &ad_slot); + + state + .tera + .render("next_ad.html", &context) + .expect("Should render") + }; + + Ok(Html(html)) +} + +/// It takes a type like `legacy_300x250` and returns a [`Size`] struct with size of `300x250`. +fn size_from_type(ad_type: &str) -> anyhow::Result { + let size_str = ad_type + .strip_prefix("legacy_") + .ok_or_else(|| anyhow!("Missing `legacy_` prefix"))?; + + let (width_str, height_str) = size_str + .split_once('x') + .ok_or_else(|| anyhow!("Width and height should be separated by `x`"))?; + + match (width_str.parse::(), height_str.parse::()) { + (Ok(width), Ok(height)) => Ok(Size::new(width, height)), + (Ok(_), Err(_err)) => bail!("Height `{height_str}` failed to be parse as u64"), + (Err(_err), Ok(_)) => bail!("Width `{width_str}` failed to be parse as u64"), + (Err(_), Err(_)) => { + bail!("Width `{width_str}` and Height `{height_str}` failed to be parse as u64") + } + } +} + +/// This function is flawed because we don't parse the language itself and we +/// don't take the weights into account! +fn parse_navigator_language(accept_language: &str) -> anyhow::Result> { + if accept_language == "*" { + return Ok(None); + } + + let first_language = accept_language + .chars() + .take_while(|ch| ch != &',' && ch != &';') + .collect::(); + + if first_language.is_empty() { + return Ok(None); + } + + Ok(Some(first_language)) +} + +#[cfg(test)] +mod test { + use adview_manager::manager::Size; + + use super::{parse_navigator_language, size_from_type}; + + #[test] + fn test_parse_navigator_language() { + { + let any_language = "*"; + let result = parse_navigator_language(any_language).expect("Should parse language"); + + assert_eq!(None, result, "Any language should be matched"); + } + + { + let result = parse_navigator_language("").expect("Should parse language"); + + assert_eq!(None, result, "No language"); + } + + { + let multiple_with_factor = "fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5"; + let result = + parse_navigator_language(multiple_with_factor).expect("Should parse language"); + + assert_eq!( + Some("fr-CH".to_string()), + result, + "fr-CH language should be matched" + ); + } + + { + let english_with_factor = "en-US,en;q=0.5"; + let result = + parse_navigator_language(english_with_factor).expect("Should parse language"); + + assert_eq!( + Some("en-US".to_string()), + result, + "en-US language should be matched" + ); + } + + { + let multiple_without_factor = "en-US, zh-CN, ja-JP"; + let result = + parse_navigator_language(multiple_without_factor).expect("Should parse language"); + + assert_eq!( + Some("en-US".to_string()), + result, + "en-US language should be matched" + ); + } + + // Malformed + // We don't have any validation, so we should get back the same value + { + let malformed = "ab-CDE-ffff l -- adsfasd '"; + let result = parse_navigator_language(malformed).expect("Should parse language"); + + assert_eq!( + Some(malformed.to_string()), + result, + "The malformed language should be matched" + ); + } + } + + #[test] + fn test_size_from_type() { + let legacy = [ + (Size::new(300, 250), "legacy_300x250"), + (Size::new(250, 250), "legacy_250x250"), + (Size::new(240, 400), "legacy_240x400"), + (Size::new(336, 280), "legacy_336x280"), + (Size::new(180, 150), "legacy_180x150"), + (Size::new(300, 100), "legacy_300x100"), + (Size::new(720, 300), "legacy_720x300"), + (Size::new(468, 60), "legacy_468x60"), + (Size::new(234, 60), "legacy_234x60"), + (Size::new(88, 31), "legacy_88x31"), + (Size::new(120, 90), "legacy_120x90"), + (Size::new(120, 60), "legacy_120x60"), + (Size::new(120, 240), "legacy_120x240"), + (Size::new(125, 125), "legacy_125x125"), + (Size::new(728, 90), "legacy_728x90"), + (Size::new(160, 600), "legacy_160x600"), + (Size::new(120, 600), "legacy_120x600"), + (Size::new(300, 600), "legacy_300x600"), + ]; + + for (expected, type_string) in legacy { + let actual_size = size_from_type(type_string).expect("Should parse Ad type"); + pretty_assertions::assert_eq!( + expected, + actual_size, + "Expected Size does not match with the parsed one" + ); + } + } +} diff --git a/adview-manager/serve/templates/base.html b/adview-manager/serve/templates/base.html index ec13a962c..98e5b9f61 100644 --- a/adview-manager/serve/templates/base.html +++ b/adview-manager/serve/templates/base.html @@ -17,6 +17,15 @@ display: flex; } + .flex-container { + display: flex; + flex-wrap: wrap; + } + + .flex-6 { + flex: 50%; + } + .h-100 { height: 100%; } diff --git a/adview-manager/serve/templates/next_ad.html b/adview-manager/serve/templates/next_ad.html new file mode 100644 index 000000000..f972f300a --- /dev/null +++ b/adview-manager/serve/templates/next_ad.html @@ -0,0 +1,29 @@ +{% extends "base.html" %} + +{% block title %}Serve an Ad{% endblock %} + +{% block content %} +
+
+ {% if next_ad_unit %} + + {{ next_ad_unit | json_encode }} + + {% else %} +
No matched AdUnit
+ {% endif %} + + {{ ad_slot | json_encode }} + +
+
+
+ {% if next_ad_unit %} + {{ next_ad_unit.html | safe }} + {% else %} +

No matched AdUnit

+ {% endif %} +
+
+
+{% endblock %} \ No newline at end of file diff --git a/adview-manager/src/helpers.rs b/adview-manager/src/helpers.rs index b6995de29..bc68c105c 100644 --- a/adview-manager/src/helpers.rs +++ b/adview-manager/src/helpers.rs @@ -156,7 +156,7 @@ pub fn get_unit_html_with_events( let body = serde_json::to_string(&events_body).expect("It should always serialize EventBody"); - // TODO: check whether the JSON body with `''` quotes executes correctly! + // Be careful with JSON body and always use `'` quotes. let fetch_opts = format!("var fetchOpts = {{ method: 'POST', headers: {{ 'content-type': 'application/json' }}, body: {body} }};"); let validators: String = validators diff --git a/adview-manager/src/manager.rs b/adview-manager/src/manager.rs index 05ad490a2..6862adb38 100644 --- a/adview-manager/src/manager.rs +++ b/adview-manager/src/manager.rs @@ -12,7 +12,6 @@ use async_std::{sync::RwLock, task::block_on}; use chrono::{DateTime, Duration, Utc}; use log::error; use once_cell::sync::Lazy; -use rand::Rng; use reqwest::StatusCode; use serde::{Deserialize, Serialize}; use std::{ @@ -63,12 +62,11 @@ pub enum Error { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] pub struct Options { - #[serde(rename = "marketURL")] - pub market_url: ApiUrl, pub market_slot: IPFS, pub publisher_addr: Address, /// All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared pub whitelisted_tokens: HashSet
, + /// Optional size to be set on the generated HTML for serving the ad. pub size: Option, pub navigator_language: Option, /// Whether or not to disable Video ads. @@ -99,7 +97,7 @@ impl Size { } /// The next [`AdUnit`] to be shown -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct NextAdUnit { pub unit: AdUnit, pub price: UnifiedNum, @@ -119,9 +117,13 @@ pub struct StickyAdUnit { /// History entry of impressions (won auctions) which the [`Manager`] holds. #[derive(Debug, Clone)] pub struct HistoryEntry { + /// The time when this auction was won and the ad was shown. pub time: DateTime, + /// The `AdUnit` shown pub unit_id: IPFS, + // The `Campaign` for which this `AdUnit` was show. pub campaign_id: CampaignId, + // The `AdSlot` for which the `AdUnit` was show. pub slot_id: IPFS, } @@ -267,7 +269,14 @@ impl Manager { .map_err(|_| Error::InvalidValidatorUrl)?; // Ordering of the campaigns matters so we will just push them to the first result // We reuse `targeting_input_base`, `accepted_referrers` and `fallback_unit` - let mut first_res: Response = self.client.get(url.as_str()).send().await?.json().await?; + let mut first_res: Response = self + .client + .get(url.as_str()) + .send() + .await? + .error_for_status()? + .json() + .await?; for validator in self.options.validators.iter().skip(1) { let url = validator @@ -316,9 +325,10 @@ impl Manager { } // If two or more units result in the same price, apply random selection between them: this is why we need the seed - let mut rng = rand::thread_rng(); + // let mut rng = rand::thread_rng(); + // let random: f64 = rng.gen::() * (0x80000000_u64 as f64 - 1.0); + let random: f64 = rand::random::() * (0x80000000_u64 as f64 - 1.0); - let random: f64 = rng.gen::() * (0x80000000_u64 as f64 - 1.0); let seed = BigNum::from(random as u64); // Apply targeting, now with adView.* variables, and sort the resulting ad units @@ -573,7 +583,6 @@ mod test { .await; // 2. Set up a manager - let market_url = server.uri().parse().unwrap(); let whitelisted_tokens = DEFAULT_TOKENS.clone(); let validator_1_url = @@ -583,7 +592,6 @@ mod test { let validator_3_url = ApiUrl::parse(&format!("{}/validator-3", server.uri())).expect("should parse"); let options = Options { - market_url, market_slot: DUMMY_IPFS[0], publisher_addr: *PUBLISHER, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared diff --git a/primitives/examples/ad_slot.rs b/primitives/examples/ad_slot.rs new file mode 100644 index 000000000..c846542df --- /dev/null +++ b/primitives/examples/ad_slot.rs @@ -0,0 +1,66 @@ +use primitives::AdSlot; +use serde_json::{from_value, json}; + +fn main() { + let json = json!({ + "ipfs": "QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR", + "type": "legacy_250x250", + "minPerImpression": { + // `Mocked TOKEN 1: 0.1` + "0x12a28f2bfBFfDf5842657235cC058242f40fDEa6": "10000000" + }, + "rules": [], + "fallbackUnit": null, + "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", + // milliseconds + "created": "1564372800000", + "title": "Test AdSlot", + "website": "adex.network", + "archived": false, + }); + + // let ad_slot = AdSlot { + // ipfs: DUMMY_IPFS[0], + // ad_type: "legacy_250x250".to_string(), + // archived: false, + // created: Utc.ymd(2019, 7, 29).and_hms(7, 0, 0), + // description: Some("Test slot for running integration tests".to_string()), + // fallback_unit: Some(fallback_unit.ipfs), + // min_per_impression: Some( + // [ + // ( + // GANACHE_INFO_1.tokens["Mocked TOKEN 1"].address, + // UnifiedNum::from_whole(0.010), + // ), + // ( + // GANACHE_INFO_1337.tokens["Mocked TOKEN 1337"].address, + // UnifiedNum::from_whole(0.001), + // ), + // ] + // .into_iter() + // .collect(), + // ), + // modified: Some(Utc.ymd(2019, 7, 29).and_hms(7, 0, 0)), + // owner: IDS[&PUBLISHER], + // title: Some("Test slot 1".to_string()), + // website: Some("https://adex.network".to_string()), + // rules: Rules::default(), + // } + + // let expected = AdSlot { + // ipfs: DUMMY_IPFS[0], + // ad_type: String, + // min_per_impression: Option>, + // rules: Rules::default(), + // fallback_unit: Some(), + // owner: ValidatorId, + // created: DateTime, + // title: Option, + // description: Option, + // website: Option, + // archived: false, + // modified: Option>, + // }; + + assert!(from_value::(json).is_ok()); +} From 06fdad14a809b4b7dbc28a5b0e0aa9ed3864c516 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 25 Oct 2022 20:41:54 +0300 Subject: [PATCH 05/15] sentry - units-for-slot - add path for slot IPFS Signed-off-by: Lachezar Lechev --- sentry/src/routes/routers.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/routes/routers.rs b/sentry/src/routes/routers.rs index 565bfa076..6d3c878f7 100644 --- a/sentry/src/routes/routers.rs +++ b/sentry/src/routes/routers.rs @@ -176,7 +176,7 @@ pub fn campaigns_router() -> Router { /// `/v5/units-for-slot` router pub fn units_for_slot_router() -> Router { - Router::new().route("/", get(get_units_for_slot::)) + Router::new().route("/:slot", get(get_units_for_slot::)) } /// `/v5/analytics` router From adbc359d9ab7daf2bd3ddfda3cf984eb23985fbd Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 25 Oct 2022 20:42:20 +0300 Subject: [PATCH 06/15] primitives - platform - Website fields to camelCase Signed-off-by: Lachezar Lechev --- primitives/src/platform.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/primitives/src/platform.rs b/primitives/src/platform.rs index 393830b8b..4e2961711 100644 --- a/primitives/src/platform.rs +++ b/primitives/src/platform.rs @@ -4,6 +4,7 @@ use url::Url; use crate::{AdSlot, AdUnit}; #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] pub struct Website { #[serde(default)] pub categories: Vec, From 909badb7c54b41938d37e196931b9c2c6c8fbea7 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 25 Oct 2022 20:42:40 +0300 Subject: [PATCH 07/15] config - ganache - add locally running platform url Signed-off-by: Lachezar Lechev --- docs/config/ganache.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config/ganache.toml b/docs/config/ganache.toml index 2f32729f7..4fd23f00f 100644 --- a/docs/config/ganache.toml +++ b/docs/config/ganache.toml @@ -51,7 +51,7 @@ admins = [ [platform] # This should be changed for tests and use the wiremock url -url = "https://platform.adex.network" +url = "http://localhost:8004" # 20 minutes in milliseconds keep_alive_interval = 1200000 From a82785c65e672d3c83e7d66845716e6b2ba36baf Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 25 Oct 2022 20:47:17 +0300 Subject: [PATCH 08/15] test_harness - platform mock server: - adview - serve - Cargo - add reqwest for mock platform calls - adview - serve - Cargo - move pretty_assertions to dev. deps - platform mock - simple mocking POST request and GET for the AdSlot Signed-off-by: Lachezar Lechev --- Cargo.lock | 16 +++++++++ Cargo.toml | 2 ++ adview-manager/serve/Cargo.toml | 4 +++ test_harness/platform/Cargo.toml | 34 ++++++++++++++++++ test_harness/platform/src/main.rs | 57 +++++++++++++++++++++++++++++++ 5 files changed, 113 insertions(+) create mode 100644 test_harness/platform/Cargo.toml create mode 100644 test_harness/platform/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 28590c288..02045053e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,6 +69,7 @@ dependencies = [ "chrono", "pretty_assertions", "primitives", + "reqwest", "serde", "tera", "tokio", @@ -2596,6 +2597,21 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "platform" +version = "0.1.0" +dependencies = [ + "anyhow", + "axum", + "dashmap", + "pretty_assertions", + "primitives", + "serde", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "polling" version = "2.3.0" diff --git a/Cargo.toml b/Cargo.toml index 0b1d232d1..6c66bd78b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,4 +9,6 @@ members = [ "validator_worker", "sentry", "test_harness", + # for mocking calls to the platform + "test_harness/platform", ] diff --git a/adview-manager/serve/Cargo.toml b/adview-manager/serve/Cargo.toml index 646b68608..a4279af5f 100644 --- a/adview-manager/serve/Cargo.toml +++ b/adview-manager/serve/Cargo.toml @@ -18,6 +18,9 @@ anyhow = "1" # Time chrono = "0.4" +# Making requests to the Mocked Platform +reqwest = { version = "0.11", features = ["json"] } + # Async runtime tokio = { version = "1", features = ["macros", "time", "rt-multi-thread"] } @@ -37,4 +40,5 @@ serde = { version = "^1.0", features = ["derive"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +[dev-dependencies] pretty_assertions = "1" diff --git a/test_harness/platform/Cargo.toml b/test_harness/platform/Cargo.toml new file mode 100644 index 000000000..1a0dbc150 --- /dev/null +++ b/test_harness/platform/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "platform" +authors = ["Ambire ", "Lachezar Lechev "] +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# Domain +adex_primitives = { version = "0.2.0", path = "../../primitives", package = "primitives", features = ["test-util"] } + +# Application errors +anyhow = "1" + +# Async runtime +tokio = { version = "1", features = ["macros", "time", "rt-multi-thread"] } + +# Web Server +axum = {version = "0.5", features = ["headers", "macros"]} + +# (De)Serialization +serde = { version = "^1.0", features = ["derive"] } + +# In-memory database +dashmap = "5" + +# Tracing +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } + +pretty_assertions = "1" diff --git a/test_harness/platform/src/main.rs b/test_harness/platform/src/main.rs new file mode 100644 index 000000000..e4c8cc73e --- /dev/null +++ b/test_harness/platform/src/main.rs @@ -0,0 +1,57 @@ +use std::{net::SocketAddr, sync::Arc}; + +use adex_primitives::{platform::AdSlotResponse, IPFS}; +use axum::{ + extract::Path, + http::StatusCode, + response::{IntoResponse, Response}, + routing::{get, post}, + Extension, Json, Router, Server, +}; +use dashmap::DashMap; +use tracing::info; + +pub type MockedResponses = DashMap; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let tracing_subscriber = tracing_subscriber::FmtSubscriber::new(); + tracing::subscriber::set_global_default(tracing_subscriber) + .expect("setting tracing default failed"); + + let slot_responses = Arc::new(MockedResponses::new()); + + // build our application with a single router + let app = Router::new() + .route("/slot", post(mock_slot_response)) + .route("/slot/:ipfs", get(get_slot)) + .layer(Extension(slot_responses)); + + let socket_addr: SocketAddr = ([127, 0, 0, 1], 8004).into(); + info!("Server running on: {socket_addr}"); + + Server::bind(&socket_addr) + .serve(app.into_make_service()) + .await?; + + Ok(()) +} + +async fn get_slot( + Extension(responses): Extension>, + Path(slot): Path, +) -> Response { + match responses.get(&slot) { + Some(slot) => (StatusCode::OK, Json(slot.value().clone())).into_response(), + None => (StatusCode::NOT_FOUND, "Slot not found").into_response(), + } +} + +async fn mock_slot_response( + Extension(responses): Extension>, + Json(mocked_response): Json, +) -> impl IntoResponse { + responses.insert(mocked_response.slot.ipfs, mocked_response); + + StatusCode::OK +} From d5c8ad2e7769c2c1dfb7fe2562be13251e37c8d9 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 25 Oct 2022 20:47:31 +0300 Subject: [PATCH 09/15] adview - serve - routes - setup mock Platform response Signed-off-by: Lachezar Lechev --- adview-manager/serve/src/routes.rs | 101 ++++++++++------------------- 1 file changed, 35 insertions(+), 66 deletions(-) diff --git a/adview-manager/serve/src/routes.rs b/adview-manager/serve/src/routes.rs index 7646f46aa..ff5ad1bc7 100644 --- a/adview-manager/serve/src/routes.rs +++ b/adview-manager/serve/src/routes.rs @@ -7,6 +7,7 @@ use axum::{ Extension, Json, }; use chrono::Utc; +use reqwest::Client; use tera::Context; use tracing::debug; use wiremock::{ @@ -16,10 +17,12 @@ use wiremock::{ use adex_primitives::{ config::GANACHE_CONFIG, + platform::{AdSlotResponse, Website}, sentry::{units_for_slot, IMPRESSION}, targeting::{input::Global, Input}, test_util::{ - DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, DUMMY_VALIDATOR_LEADER, PUBLISHER, + DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, + DUMMY_VALIDATOR_LEADER, PUBLISHER, }, util::ApiUrl, AdSlot, ToHex, @@ -47,9 +50,7 @@ pub async fn get_preview_ad(Extension(state): Extension>) -> Html>) -> Html>) -> Html { let whitelisted_tokens = DEFAULT_TOKENS.clone(); let disabled_video = false; - let publisher_addr = "0x0000000000000000626f62627973686d75726461" - .parse() - .unwrap(); + let publisher_addr = *PUBLISHER; let options = Options { market_slot: DUMMY_IPFS[0], @@ -206,65 +205,6 @@ pub async fn get_preview_video(Extension(state): Extension>) -> Html< Html(html) } -/// `GET /preview/:slot` -// pub async fn get_slot_preview( -// Extension(state): Extension>, -// Path(slot): Path, -// headers: HeaderMap, -// ) -> Result, Error> { -// let config = GANACHE_CONFIG.clone(); - -// // extracted from Accept-language header -// let navigator_language = headers -// .get(ACCEPT_LANGUAGE) -// .map(|value| value.to_str()) -// .transpose()? -// .map(|s| parse_navigator_language(s)) -// .transpose()? -// .flatten() -// // TODO: make configurable? -// .unwrap_or("en".into()); - -// let whitelisted_tokens = config -// .chains -// .iter() -// .map(|(_, chain)| chain.tokens.iter().map(|(_, token)| token.address)) -// .flatten() -// .collect(); - -// let options = Options { -// market_slot: ad_slot.ipfs, -// publisher_addr: *PUBLISHER, -// // All passed tokens must be of the same price, so that the amounts can be accurately compared -// whitelisted_tokens, -// size: Some( -// size_from_type(&ad_slot.ad_type) -// .map_err(|error| Error::anyhow_status(error, StatusCode::BAD_REQUEST))?, -// ), -// navigator_language: Some(navigator_language), -// /// Defaulted -// disabled_video: false, -// disabled_sticky: false, -// }; - -// let manager = Manager::new(options, Default::default())?; - -// let next_ad_unit = manager.get_next_ad_unit().await?; - -// let html = { -// let mut context = Context::new(); -// context.insert("next_ad_unit", &next_ad_unit); -// context.insert("ad_slot", &ad_slot); - -// state -// .tera -// .render("next_ad.html", &context) -// .expect("Should render") -// }; - -// Ok(Html(html)) -// } - /// `POST /preview` /// /// Uses the provided with the POST data [`AdSlot`] and get's a matching [`AdUnit`] html @@ -272,6 +212,7 @@ pub async fn get_preview_video(Extension(state): Extension>) -> Html< /// /// Uses the Ganache config to select all the whitelisted tokens in all chains. /// It's configured to use locally running sentry validators at ports `8005` and `8006`. +/// Alongside locally running mocked platform at `8004`. #[axum::debug_handler] pub async fn post_slot_preview( Extension(state): Extension>, @@ -280,6 +221,21 @@ pub async fn post_slot_preview( ) -> Result, Error> { let config = GANACHE_CONFIG.clone(); + // setup the `AdSlotResponse` from the Platform + let response = AdSlotResponse { + slot: ad_slot.clone(), + fallback: Some(DUMMY_AD_UNITS[0].clone()), + website: Some(Website { + categories: vec![ + "IAB3".to_string(), + "IAB13-7".to_string(), + "IAB5".to_string(), + ], + accepted_referrers: vec![], + }), + }; + setup_platform_response(&response).await?; + // extracted from Accept-language header let navigator_language = headers .get(ACCEPT_LANGUAGE) @@ -373,6 +329,19 @@ fn parse_navigator_language(accept_language: &str) -> anyhow::Result anyhow::Result<()> { + let client = Client::builder().build()?; + + client + .post("http://localhost:8004/slot") + .json(&response) + .send() + .await? + .error_for_status()?; + + Ok(()) +} + #[cfg(test)] mod test { use adview_manager::manager::Size; From 3bc94fea8d9087bc6abaf776a286e75088e1b44f Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 31 Oct 2022 17:05:53 +0200 Subject: [PATCH 10/15] sentry - CORS - allow all any header Signed-off-by: Lachezar Lechev --- sentry/src/application.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sentry/src/application.rs b/sentry/src/application.rs index 8dda33467..fc6ec8a26 100644 --- a/sentry/src/application.rs +++ b/sentry/src/application.rs @@ -146,7 +146,10 @@ where ]) // allow requests from any origin // "*" - .allow_origin(tower_http::cors::Any); + .allow_origin(tower_http::cors::Any) + // todo: Check whether we should support only the `content-type` + // instead of `Access-Control-Allow-Headers: *` + .allow_headers(tower_http::cors::Any); let router = Router::new() .nest("/channel", channels_router::()) From 683145fdec92717c35b3e92fc2cce2756a598b76 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 31 Oct 2022 17:07:03 +0200 Subject: [PATCH 11/15] primitives - fix test_util AdUnit Signed-off-by: Lachezar Lechev --- primitives/src/targeting/eval.rs | 1 - primitives/src/targeting/input.rs | 1 - primitives/src/test_util.rs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/primitives/src/targeting/eval.rs b/primitives/src/targeting/eval.rs index c507c1fb9..98cf0be5a 100644 --- a/primitives/src/targeting/eval.rs +++ b/primitives/src/targeting/eval.rs @@ -205,7 +205,6 @@ impl From for SerdeValue { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(rename_all = "camelCase")] -// TODO: https://github.com/AdExNetwork/adex-validator-stack-rust/issues/296 pub enum Function { /// Multiplies first two values and then divides product by third value MulDiv(Box, Box, Box), diff --git a/primitives/src/targeting/input.rs b/primitives/src/targeting/input.rs index 5ba911915..b466de1b6 100644 --- a/primitives/src/targeting/input.rs +++ b/primitives/src/targeting/input.rs @@ -140,7 +140,6 @@ impl GetField for AdView { #[serde(rename_all = "camelCase")] /// Global scope, accessible everywhere pub struct Global { - /// We still use `String`, because the `Event`s have an `Option`al `AdSlot` value. pub ad_slot_id: IPFS, pub ad_slot_type: String, pub publisher_id: Address, diff --git a/primitives/src/test_util.rs b/primitives/src/test_util.rs index f76a455a9..c98478358 100644 --- a/primitives/src/test_util.rs +++ b/primitives/src/test_util.rs @@ -228,7 +228,7 @@ pub static DUMMY_AD_UNITS: Lazy<[AdUnit; 4]> = Lazy::new(|| { target_url: "https://www.adex.network/?stremio-test-banner-1".to_string(), archived: false, description: Some("Dummy AdUnit description 1".to_string()), - ad_type: "legacy_250x250".to_string(), + ad_type: "legacy_300x100".to_string(), /// Timestamp: 1 564 383 600 created: Utc.ymd(2019, 7, 29).and_hms(9, 0, 0), min_targeting_score: None, From 7aeff67e1d4fc184f2f3b3972b6eaa8f254ebc00 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 31 Oct 2022 17:12:35 +0200 Subject: [PATCH 12/15] primitives - example - update get /cfg example Signed-off-by: Lachezar Lechev --- primitives/examples/get_cfg_response.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/primitives/examples/get_cfg_response.rs b/primitives/examples/get_cfg_response.rs index e9cdbce35..42697408f 100644 --- a/primitives/examples/get_cfg_response.rs +++ b/primitives/examples/get_cfg_response.rs @@ -65,7 +65,7 @@ fn main() { } }, "platform": { - "url": "https://platform.adex.network/", + "url": "http://localhost:8004", "keep_alive_interval": 1200000 }, "limits": { From 1e00a28b4aac67b2ef8e0a43c497f78f49a2b98e Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Mon, 31 Oct 2022 17:18:05 +0200 Subject: [PATCH 13/15] adview serve - previewing a custom AdSlot & AdUnit Signed-off-by: Lachezar Lechev --- Cargo.lock | 38 +++ adview-manager/README.md | 13 +- adview-manager/serve/Cargo.toml | 11 +- adview-manager/serve/src/app.rs | 130 +++++---- adview-manager/serve/src/main.rs | 8 +- adview-manager/serve/src/routes.rs | 268 +++++++++++------- adview-manager/serve/templates/base.html | 4 + adview-manager/serve/templates/index.html | 1 + adview-manager/serve/templates/next_ad.html | 32 ++- .../serve/templates/preview_form.html | 58 ++++ 10 files changed, 388 insertions(+), 175 deletions(-) create mode 100644 adview-manager/serve/templates/preview_form.html diff --git a/Cargo.lock b/Cargo.lock index 02045053e..471b68204 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,11 +66,15 @@ dependencies = [ "adview-manager", "anyhow", "axum", + "axum-extra", "chrono", + "envy", + "once_cell", "pretty_assertions", "primitives", "reqwest", "serde", + "serde_json", "tera", "tokio", "tracing", @@ -381,6 +385,27 @@ dependencies = [ "tower-service", ] +[[package]] +name = "axum-extra" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69034b3b0fd97923eee2ce8a47540edb21e07f48f87f67d44bb4271cec622bdb" +dependencies = [ + "axum", + "bytes", + "futures-util", + "http", + "mime", + "pin-project-lite", + "serde", + "serde_html_form", + "tokio", + "tower", + "tower-http", + "tower-layer", + "tower-service", +] + [[package]] name = "axum-macros" version = "0.2.3" @@ -3374,6 +3399,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_html_form" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3312ac3bf56e70cb7082a85db89d127940607b6acf39bd537cc06c8212124927" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_json" version = "1.0.86" diff --git a/adview-manager/README.md b/adview-manager/README.md index 516bb8d05..e1e7f83e1 100644 --- a/adview-manager/README.md +++ b/adview-manager/README.md @@ -22,6 +22,15 @@ for the server. Routes: - `GET /` - landing page +- `GET /preview` - preview form for submitting a single AdSlot response from `platform` and see the result +- `POST /preview` - preview of a single AdSlot Image example - `GET /preview/ad` - preview a single example ad -- `GET /preview/video` - preview a single example ad -- `POST /:slot` - preview a single example ad \ No newline at end of file +- `GET /preview/video` - preview a single Video example ad + + +### POST `/preview` + +You need to be running: +- `sentry` Leader at 8005 +- `sentry` Follower at 8006 +- Mock [`Platform`](../test_harness/platform) at 8004 \ No newline at end of file diff --git a/adview-manager/serve/Cargo.toml b/adview-manager/serve/Cargo.toml index a4279af5f..bc6b44482 100644 --- a/adview-manager/serve/Cargo.toml +++ b/adview-manager/serve/Cargo.toml @@ -25,7 +25,8 @@ reqwest = { version = "0.11", features = ["json"] } tokio = { version = "1", features = ["macros", "time", "rt-multi-thread"] } # Web Server -axum = {version = "0.5", features = ["headers", "macros"]} +axum = { version = "0.5", features = ["headers", "macros"] } +axum-extra = { version = "0.3", features = ["form"] } # Template engine tera = { version = "1" } @@ -35,10 +36,16 @@ wiremock = { version = "0.5" } # (De)Serialization serde = { version = "^1.0", features = ["derive"] } +serde_json = "1" + +# For env. variable deserialization for Config +envy = "0.4" # Tracing -tracing = "0.1" +tracing = { version = "0.1", features = ["log"] } tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +once_cell = "1" + [dev-dependencies] pretty_assertions = "1" diff --git a/adview-manager/serve/src/app.rs b/adview-manager/serve/src/app.rs index e1503af44..d4588edd3 100644 --- a/adview-manager/serve/src/app.rs +++ b/adview-manager/serve/src/app.rs @@ -1,16 +1,19 @@ -use std::{net::SocketAddr, sync::Arc}; - -use axum::{ - http::StatusCode, - response::IntoResponse, - routing::{get, post}, - Extension, Router, Server, +use std::{ + net::{IpAddr, Ipv4Addr, SocketAddr}, + sync::Arc, }; + +use axum::{http::StatusCode, response::IntoResponse, routing::get, Extension, Router, Server}; +use serde::Deserialize; use tracing::{error, info}; use tera::Tera; -use crate::routes::{get_index, get_preview_ad, get_preview_video, post_slot_preview}; +use crate::routes::{ + get_index, get_preview_ad, get_preview_video, get_slot_preview_form, post_slot_preview, +}; + +pub use envy::Error as EnvError; #[derive(Debug)] pub struct State { @@ -18,8 +21,61 @@ pub struct State { } pub struct Application { - /// The shared state of the application + /// The shared state of the application. state: Arc, + /// Configuration values taken from the environment variables. + env_config: EnvConfig, +} + +impl Application { + pub fn new() -> Result> { + let serve_dir = match std::env::current_dir().unwrap() { + serve_path if serve_path.ends_with("serve") => serve_path, + adview_manager_path if adview_manager_path.ends_with("adview-manager") => { + adview_manager_path.join("serve") + } + // running from the Validator stack workspace + workspace_path => workspace_path.join("adview-manager/serve"), + }; + + let templates_glob = format!("{}/templates/**/*.html", serve_dir.display()); + + info!("Tera templates glob path: {templates_glob}"); + // Use globbing + let tera = Tera::new(&templates_glob)?; + + let env_config = EnvConfig::from_env()?; + + let shared_state = Arc::new(State { tera }); + + Ok(Self { + state: shared_state, + env_config, + }) + } + + pub async fn run(&self) -> Result<(), Box> { + let preview_routes = Router::new() + .route("/", get(get_slot_preview_form).post(post_slot_preview)) + .route("/ad", get(get_preview_ad)) + .route("/video", get(get_preview_video)); + + // build our application with a single route + let app = Router::new() + .route("/", get(get_index)) + .nest("/preview", preview_routes) + .layer(Extension(self.state.clone())); + + let socket_addr: SocketAddr = SocketAddr::new(self.env_config.ip, self.env_config.port); + info!("Server running on: {socket_addr}"); + + // run it with hyper on the socket address + Server::bind(&socket_addr) + .serve(app.into_make_service()) + .await?; + + Ok(()) + } } pub struct Error { @@ -82,50 +138,24 @@ where } } -impl Application { - pub fn new() -> Result> { - let serve_dir = match std::env::current_dir().unwrap() { - serve_path if serve_path.ends_with("serve") => serve_path, - adview_manager_path if adview_manager_path.ends_with("adview-manager") => { - adview_manager_path.join("serve") - } - // running from the Validator stack workspace - workspace_path => workspace_path.join("adview-manager/serve"), - }; - - let templates_glob = format!("{}/templates/**/*.html", serve_dir.display()); - - info!("Tera templates glob path: {templates_glob}"); - // Use globbing - let tera = Tera::new(&templates_glob)?; - - let shared_state = Arc::new(State { tera }); +#[derive(Deserialize, Debug, Clone, Copy)] +pub struct EnvConfig { + #[serde(default = "EnvConfig::default_ip")] + ip: IpAddr, + #[serde(default = "EnvConfig::default_port")] + port: u16, +} - Ok(Self { - state: shared_state, - }) +impl EnvConfig { + pub fn from_env() -> Result { + envy::from_env() } - pub async fn run(&self) -> Result<(), Box> { - let preview_routes = Router::new() - .route("/", post(post_slot_preview)) - .route("/ad", get(get_preview_ad)) - .route("/video", get(get_preview_video)); - - // build our application with a single route - let app = Router::new() - .route("/", get(get_index)) - .nest("/preview", preview_routes) - .layer(Extension(self.state.clone())); - - let socket_addr: SocketAddr = ([127, 0, 0, 1], 3030).into(); - info!("Server running on: {socket_addr}"); - - // run it with hyper on localhost:3030 - Server::bind(&socket_addr) - .serve(app.into_make_service()) - .await?; + pub fn default_port() -> u16 { + 8001 + } - Ok(()) + pub fn default_ip() -> IpAddr { + IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) } } diff --git a/adview-manager/serve/src/main.rs b/adview-manager/serve/src/main.rs index 42c68bd9f..f9ce8c924 100644 --- a/adview-manager/serve/src/main.rs +++ b/adview-manager/serve/src/main.rs @@ -1,10 +1,12 @@ use adview_serve::app::Application; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; #[tokio::main] async fn main() -> Result<(), Box> { - let tracing_subscriber = tracing_subscriber::FmtSubscriber::new(); - tracing::subscriber::set_global_default(tracing_subscriber) - .expect("setting tracing default failed"); + tracing_subscriber::registry() + .with(fmt::layer()) + .with(EnvFilter::from_default_env()) + .init(); Application::new()?.run().await?; diff --git a/adview-manager/serve/src/routes.rs b/adview-manager/serve/src/routes.rs index ff5ad1bc7..8c333c9db 100644 --- a/adview-manager/serve/src/routes.rs +++ b/adview-manager/serve/src/routes.rs @@ -1,38 +1,42 @@ -use std::sync::Arc; +use std::{collections::HashSet, fmt::Display, ops::Deref, str::FromStr, sync::Arc}; use anyhow::{anyhow, bail}; use axum::{ http::{header::ACCEPT_LANGUAGE, HeaderMap, StatusCode}, response::Html, - Extension, Json, + Extension, }; -use chrono::Utc; +use axum_extra::extract::Form; +use chrono::{TimeZone, Utc}; +use once_cell::sync::Lazy; use reqwest::Client; +use serde::{de::Error as _, Deserialize, Deserializer, Serialize}; use tera::Context; -use tracing::debug; -use wiremock::{ - matchers::{method, path, query_param}, - Mock, MockServer, ResponseTemplate, -}; use adex_primitives::{ config::GANACHE_CONFIG, platform::{AdSlotResponse, Website}, - sentry::{units_for_slot, IMPRESSION}, - targeting::{input::Global, Input}, + targeting::Rules, test_util::{ DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, - DUMMY_VALIDATOR_LEADER, PUBLISHER, + DUMMY_VALIDATOR_LEADER, IDS, PUBLISHER, }, util::ApiUrl, - AdSlot, ToHex, -}; -use adview_manager::{ - get_unit_html_with_events, manager::Size, manager::DEFAULT_TOKENS, Manager, Options, + AdSlot, Address, }; +use adview_manager::{get_unit_html_with_events, manager::Size, Manager, Options}; use crate::app::{Error, State}; +/// All the configured tokens in the `ganache.toml` config file +pub static WHITELISTED_TOKENS: Lazy> = Lazy::new(|| { + GANACHE_CONFIG + .chains + .values() + .flat_map(|chain| chain.tokens.values().map(|token| token.address)) + .collect() +}); + /// `GET /` pub async fn get_index(Extension(state): Extension>) -> Html { let html = state @@ -45,80 +49,28 @@ pub async fn get_index(Extension(state): Extension>) -> Html /// `GET /preview/ad` pub async fn get_preview_ad(Extension(state): Extension>) -> Html { - // For mocking the `get_units_for_slot_resp` call - let mock_server = MockServer::start().await; - - let whitelisted_tokens = DEFAULT_TOKENS.clone(); let disabled_video = false; let publisher_addr = *PUBLISHER; + let campaign = DUMMY_CAMPAIGN.clone(); + // ordering matters + let validators_url = vec![ + ApiUrl::parse(&campaign.leader().unwrap().url).expect("should parse"), + ApiUrl::parse(&campaign.leader().unwrap().url).expect("should parse"), + ]; let options = Options { market_slot: DUMMY_IPFS[0], publisher_addr, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared - whitelisted_tokens, + whitelisted_tokens: WHITELISTED_TOKENS.clone(), size: Some(Size::new(300, 100)), - // TODO: Check this value navigator_language: Some("bg".into()), /// Defaulted disabled_video, disabled_sticky: false, - validators: vec![ - ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"), - ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"), - ], + validators: validators_url, }; - let manager = - Manager::new(options.clone(), Default::default()).expect("Failed to create AdView Manager"); - let pub_prefix = publisher_addr.to_hex(); - - let units_for_slot_resp = units_for_slot::response::Response { - targeting_input_base: Input { - ad_view: None, - global: Global { - ad_slot_id: options.market_slot, - ad_slot_type: "".into(), - publisher_id: publisher_addr, - country: Some("Bulgaria".into()), - event_type: IMPRESSION, - seconds_since_epoch: Utc::now(), - user_agent_os: None, - user_agent_browser_family: None, - }, - campaign: None, - balances: None, - ad_unit_id: None, - ad_slot: None, - }, - accepted_referrers: vec![], - fallback_unit: None, - campaigns: vec![], - }; - - // Mock the `get_units_for_slot_resp` call - let mock_call = Mock::given(method("GET")) - .and(path(format!("units-for-slot/{}", options.market_slot))) - // pubPrefix=HEX&depositAssets[]=0xASSET1&depositAssets[]=0xASSET2 - .and(query_param("pubPrefix", pub_prefix)) - .and(query_param( - "depositAssets[]", - "0x6B175474E89094C44Da98b954EedeAC495271d0F", - )) - .respond_with(ResponseTemplate::new(200).set_body_json(units_for_slot_resp)) - .expect(1) - .named("get_units_for_slot_resp"); - - // Mounting the mock on the mock server - it's now effective! - mock_call.mount(&mock_server).await; - - let demand_resp = manager - .get_units_for_slot_resp() - .await - .expect("Should return Mocked response"); - - debug!("Mocked response: {demand_resp:?}"); - let ufs_ad_unit = adex_primitives::sentry::units_for_slot::response::AdUnit { /// Same as `ipfs` ipfs: DUMMY_IPFS[1], @@ -131,8 +83,8 @@ pub async fn get_preview_ad(Extension(state): Extension>) -> Html>) -> Html>) -> Html { - let whitelisted_tokens = DEFAULT_TOKENS.clone(); let disabled_video = false; let publisher_addr = *PUBLISHER; @@ -159,9 +110,8 @@ pub async fn get_preview_video(Extension(state): Extension>) -> Html< market_slot: DUMMY_IPFS[0], publisher_addr, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared - whitelisted_tokens, + whitelisted_tokens: WHITELISTED_TOKENS.clone(), size: Some(Size::new(728, 90)), - // TODO: Check this value navigator_language: Some("bg".into()), /// Defaulted disabled_video, @@ -205,25 +155,108 @@ pub async fn get_preview_video(Extension(state): Extension>) -> Html< Html(html) } -/// `POST /preview` +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AdSlotPreview { + #[serde(with = "form_json")] + adslot_response: AdSlotResponse, + #[serde(default)] + disabled_video: bool, + #[serde(default)] + disabled_sticky: bool, + publisher: Address, + #[serde(deserialize_with = "empty_field_string::<_, ApiUrl>")] + validators: Vec, + #[serde(deserialize_with = "empty_field_string::<_, Address>")] + whitelisted_tokens: Vec
, +} + +mod form_json { + use serde::{ + de::{DeserializeOwned, Error as _}, + ser::Error as _, + Deserialize, Deserializer, Serialize, Serializer, + }; + + pub fn serialize(value: &T, serializer: S) -> Result + where + S: Serializer, + T: Serialize, + { + let json = serde_json::to_string(value).map_err(S::Error::custom)?; + serializer.serialize_str(&json) + } + + pub fn deserialize<'de, D, T>(deserializer: D) -> Result + where + D: Deserializer<'de>, + T: DeserializeOwned, + { + let json = String::deserialize(deserializer)?; + + serde_json::from_str::(&json).map_err(D::Error::custom) + } +} + +pub fn empty_field_string<'de, D, T>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, + T: FromStr, + ::Err: Display, +{ + let vec_of_string = >::deserialize(deserializer)?; + + vec_of_string + .into_iter() + .filter_map(|string| { + if string.is_empty() { + None + } else { + Some(string.parse::()) + } + }) + .collect::>() + .map_err(D::Error::custom) +} + +/// `GET /preview` /// -/// Uses the provided with the POST data [`AdSlot`] and get's a matching [`AdUnit`] html -/// with the manager. +/// Shows a form to submit a JSON [`AdSlot`] Response ([`AdSlotResponse`]) +/// from the platform and preview the html with the matching of an [`AdUnit`] +/// using the manager. /// /// Uses the Ganache config to select all the whitelisted tokens in all chains. /// It's configured to use locally running sentry validators at ports `8005` and `8006`. /// Alongside locally running mocked platform at `8004`. +/// +/// [`AdUnit`]: adex_primitives::AdUnit #[axum::debug_handler] -pub async fn post_slot_preview( +pub async fn get_slot_preview_form( Extension(state): Extension>, - Json(ad_slot): Json, - headers: HeaderMap, ) -> Result, Error> { - let config = GANACHE_CONFIG.clone(); + // let config = GANACHE_CONFIG.clone(); + + let validators = vec![ + ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"), + ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"), + ]; + + let ad_slot = AdSlot { + ipfs: DUMMY_IPFS[0], + ad_type: "legacy_300x100".to_string(), + min_per_impression: None, + rules: Rules::default(), + fallback_unit: Some(DUMMY_AD_UNITS[0].ipfs), + owner: IDS[&PUBLISHER], + created: Utc.ymd(2019, 7, 29).and_hms(7, 0, 0), + title: Some("Test slot 1".to_string()), + description: Some("Test slot for running integration tests".to_string()), + website: Some("https://adex.network".to_string()), + archived: false, + modified: Some(Utc.ymd(2019, 7, 29).and_hms(7, 0, 0)), + }; - // setup the `AdSlotResponse` from the Platform - let response = AdSlotResponse { - slot: ad_slot.clone(), + let adslot_response = AdSlotResponse { + slot: ad_slot, fallback: Some(DUMMY_AD_UNITS[0].clone()), website: Some(Website { categories: vec![ @@ -234,7 +267,43 @@ pub async fn post_slot_preview( accepted_referrers: vec![], }), }; - setup_platform_response(&response).await?; + + let html = { + let mut context = Context::new(); + context.insert("default_publisher", &*PUBLISHER); + context.insert("default_validators", &validators); + context.insert("default_adslot_response", &adslot_response); + context.insert("default_whitelisted_tokens", WHITELISTED_TOKENS.deref()); + + state + .tera + .render("preview_form.html", &context) + .expect("Should render") + }; + + Ok(Html(html)) +} + +/// `POST /preview` +/// +/// Uses the provided with the POST data and gets a matching [`AdUnit`] html +/// with the manager. +/// +/// Uses the Ganache config to select all the whitelisted tokens in all chains. +/// It's configured to use locally running sentry validators at ports `8005` and `8006`. +/// Alongside locally running mocked platform at `8004`. +/// +/// [`AdUnit`]: adex_primitives::AdUnit +#[axum::debug_handler] +pub async fn post_slot_preview( + Extension(state): Extension>, + headers: HeaderMap, + Form(adslot_preview): Form, +) -> Result, Error> { + let ad_slot = adslot_preview.adslot_response.slot.clone(); + + // setup the `AdSlotResponse` from the Platform + setup_platform_response(&adslot_preview.adslot_response).await?; // extracted from Accept-language header let navigator_language = headers @@ -247,29 +316,20 @@ pub async fn post_slot_preview( // TODO: make configurable? .unwrap_or_else(|| "en".into()); - let whitelisted_tokens = config - .chains - .iter() - .flat_map(|(_, chain)| chain.tokens.iter().map(|(_, token)| token.address)) - .collect(); - let options = Options { market_slot: ad_slot.ipfs, publisher_addr: *PUBLISHER, // All passed tokens must be of the same price, so that the amounts can be accurately compared - whitelisted_tokens, + whitelisted_tokens: adslot_preview.whitelisted_tokens.into_iter().collect(), size: Some( size_from_type(&ad_slot.ad_type) .map_err(|error| Error::anyhow_status(error, StatusCode::BAD_REQUEST))?, ), navigator_language: Some(navigator_language), /// Defaulted - disabled_video: false, - disabled_sticky: false, - validators: vec![ - ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"), - ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"), - ], + disabled_video: adslot_preview.disabled_video, + disabled_sticky: adslot_preview.disabled_sticky, + validators: adslot_preview.validators, }; let manager = Manager::new(options, Default::default())?; diff --git a/adview-manager/serve/templates/base.html b/adview-manager/serve/templates/base.html index 98e5b9f61..6847295ed 100644 --- a/adview-manager/serve/templates/base.html +++ b/adview-manager/serve/templates/base.html @@ -26,6 +26,10 @@ flex: 50%; } + .flex-12 { + flex: 100%; + } + .h-100 { height: 100%; } diff --git a/adview-manager/serve/templates/index.html b/adview-manager/serve/templates/index.html index d7753cab7..e5ee5dd91 100644 --- a/adview-manager/serve/templates/index.html +++ b/adview-manager/serve/templates/index.html @@ -7,6 +7,7 @@

AdEx AdView Manager by Ambire

Preview the capabilities and usage of the AdView Manager

diff --git a/adview-manager/serve/templates/next_ad.html b/adview-manager/serve/templates/next_ad.html index f972f300a..1866d7d8a 100644 --- a/adview-manager/serve/templates/next_ad.html +++ b/adview-manager/serve/templates/next_ad.html @@ -3,20 +3,8 @@ {% block title %}Serve an Ad{% endblock %} {% block content %} -
-
- {% if next_ad_unit %} - - {{ next_ad_unit | json_encode }} - - {% else %} -
No matched AdUnit
- {% endif %} - - {{ ad_slot | json_encode }} - -
-
+
+
{% if next_ad_unit %} {{ next_ad_unit.html | safe }} @@ -25,5 +13,21 @@

No matched AdUnit

{% endif %}
+
+
+ {% if next_ad_unit %} + + {{ next_ad_unit | json_encode(pretty=true) }} + + {% else %} + No matched AdUnit + {% endif %} +
+
+ + {{ ad_slot | json_encode(pretty=true) }} + +
+
{% endblock %} \ No newline at end of file diff --git a/adview-manager/serve/templates/preview_form.html b/adview-manager/serve/templates/preview_form.html new file mode 100644 index 000000000..2a666ebd6 --- /dev/null +++ b/adview-manager/serve/templates/preview_form.html @@ -0,0 +1,58 @@ +{% extends "base.html" %} + +{% block title %}Preview an Ad form{% endblock %} + +{% block content %} +
+
+
+
Options
+
+ + +
+
+ + +
+
+ + +
+ {# We only create 2 validator urls for now! #} +
Validators (ordering matters - first validator has priority over second)
+ {% for i in [0, 1, 2, 3, 4] %} +
+ + +
+ {% endfor %} + +
Whitelisted token addresses
+ {% for i in [0, 1, 2, 3, 4] %} +
+ + +
+ {% endfor %} + +
+ + +
+
+ +
+
+
+ {#
+
+ {% if next_ad_unit %} + {{ next_ad_unit.html | safe }} + {% else %} +

No matched AdUnit

+ {% endif %} +
+
#} +
+{% endblock %} \ No newline at end of file From 378d61d5267af68cbba62f4f1a13a7179b440c91 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 1 Nov 2022 14:40:03 +0200 Subject: [PATCH 14/15] fix failing build and add Whitelisted tokens Signed-off-by: Lachezar Lechev --- adview-manager/serve/src/routes.rs | 17 ++---------- adview-manager/src/helpers.rs | 2 -- adview-manager/src/manager.rs | 42 ++++++++++++------------------ primitives/src/test_util.rs | 15 ++++++++++- 4 files changed, 33 insertions(+), 43 deletions(-) diff --git a/adview-manager/serve/src/routes.rs b/adview-manager/serve/src/routes.rs index 8c333c9db..022d92e89 100644 --- a/adview-manager/serve/src/routes.rs +++ b/adview-manager/serve/src/routes.rs @@ -1,4 +1,4 @@ -use std::{collections::HashSet, fmt::Display, ops::Deref, str::FromStr, sync::Arc}; +use std::{fmt::Display, ops::Deref, str::FromStr, sync::Arc}; use anyhow::{anyhow, bail}; use axum::{ @@ -8,18 +8,16 @@ use axum::{ }; use axum_extra::extract::Form; use chrono::{TimeZone, Utc}; -use once_cell::sync::Lazy; use reqwest::Client; use serde::{de::Error as _, Deserialize, Deserializer, Serialize}; use tera::Context; use adex_primitives::{ - config::GANACHE_CONFIG, platform::{AdSlotResponse, Website}, targeting::Rules, test_util::{ DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, DUMMY_VALIDATOR_FOLLOWER, - DUMMY_VALIDATOR_LEADER, IDS, PUBLISHER, + DUMMY_VALIDATOR_LEADER, IDS, PUBLISHER, WHITELISTED_TOKENS, }, util::ApiUrl, AdSlot, Address, @@ -28,15 +26,6 @@ use adview_manager::{get_unit_html_with_events, manager::Size, Manager, Options} use crate::app::{Error, State}; -/// All the configured tokens in the `ganache.toml` config file -pub static WHITELISTED_TOKENS: Lazy> = Lazy::new(|| { - GANACHE_CONFIG - .chains - .values() - .flat_map(|chain| chain.tokens.values().map(|token| token.address)) - .collect() -}); - /// `GET /` pub async fn get_index(Extension(state): Extension>) -> Html { let html = state @@ -233,8 +222,6 @@ where pub async fn get_slot_preview_form( Extension(state): Extension>, ) -> Result, Error> { - // let config = GANACHE_CONFIG.clone(); - let validators = vec![ ApiUrl::parse(&DUMMY_VALIDATOR_LEADER.url).expect("should parse"), ApiUrl::parse(&DUMMY_VALIDATOR_FOLLOWER.url).expect("should parse"), diff --git a/adview-manager/src/helpers.rs b/adview-manager/src/helpers.rs index 1ec853199..f2bc7e8b5 100644 --- a/adview-manager/src/helpers.rs +++ b/adview-manager/src/helpers.rs @@ -361,11 +361,9 @@ mod test { .flat_map(|chain| chain.tokens.values().map(|token| token.address)) .collect::>(); - let market_url = ApiUrl::parse("https://market.adex.network").expect("should parse"); let validator_1_url = ApiUrl::parse("https://tom.adex.network").expect("should parse"); let validator_2_url = ApiUrl::parse("https://jerry.adex.network").expect("should parse"); let options = Options { - market_url, market_slot: DUMMY_IPFS[0], publisher_addr: *PUBLISHER, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared diff --git a/adview-manager/src/manager.rs b/adview-manager/src/manager.rs index d65178523..45a6b07f9 100644 --- a/adview-manager/src/manager.rs +++ b/adview-manager/src/manager.rs @@ -443,12 +443,13 @@ mod test { use super::*; use crate::manager::input::Input; use adex_primitives::{ - config::GANACHE_CONFIG, sentry::{ units_for_slot::response::{AdUnit, UnitsWithPrice}, CLICK, }, - test_util::{CAMPAIGNS, DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, PUBLISHER}, + test_util::{ + CAMPAIGNS, DUMMY_AD_UNITS, DUMMY_CAMPAIGN, DUMMY_IPFS, PUBLISHER, WHITELISTED_TOKENS, + }, unified_num::FromWhole, }; use wiremock::{ @@ -456,24 +457,19 @@ mod test { Mock, MockServer, ResponseTemplate, }; - fn setup_manager(uri: String) -> Manager { - let market_url = uri.parse().unwrap(); - let whitelisted_tokens = GANACHE_CONFIG - .chains - .values() - .flat_map(|chain| chain.tokens.values().map(|token| token.address)) - .collect::>(); - - let validator_1_url = ApiUrl::parse(&format!("{}/validator-1", uri)).expect("should parse"); - let validator_2_url = ApiUrl::parse(&format!("{}/validator-2", uri)).expect("should parse"); - let validator_3_url = ApiUrl::parse(&format!("{}/validator-3", uri)).expect("should parse"); + fn setup_manager(mock_url: ApiUrl) -> Manager { + let validator_1_url = + ApiUrl::parse(&format!("{mock_url}validator-1")).expect("should parse"); + let validator_2_url = + ApiUrl::parse(&format!("{mock_url}validator-2")).expect("should parse"); + let validator_3_url = + ApiUrl::parse(&format!("{mock_url}validator-3")).expect("should parse"); let options = Options { - market_url, market_slot: DUMMY_IPFS[0], publisher_addr: *PUBLISHER, // All passed tokens must be of the same price and decimals, so that the amounts can be accurately compared - whitelisted_tokens, + whitelisted_tokens: WHITELISTED_TOKENS.clone(), size: Some(Size::new(300, 100)), navigator_language: Some("bg".into()), disabled_video: false, @@ -605,7 +601,7 @@ mod test { .await; // 2. Set up a manager - let manager = setup_manager(server.uri()); + let manager = setup_manager(server.uri().parse().unwrap()); let res = manager .get_units_for_slot_resp() @@ -619,7 +615,7 @@ mod test { #[tokio::test] async fn check_if_campaign_is_sticky() { - let mut manager = setup_manager("http://localhost:1337".to_string()); + let mut manager = setup_manager("http://localhost:8000".parse().unwrap()); // Case 1 - options has disabled sticky { @@ -627,7 +623,7 @@ mod test { assert!(!manager.is_campaign_sticky(DUMMY_CAMPAIGN.id).await); manager.options.disabled_sticky = false; } - // Case 2 - time is past stickiness treshold, less than 4 minutes ago + // Case 2 - time is past stickiness threshold, less than 4 minutes ago { let history = vec![HistoryEntry { time: Utc::now() - Duration::days(1), // 24 hours ago @@ -660,7 +656,7 @@ mod test { #[tokio::test] async fn check_sticky_ad_unit() { let server = MockServer::start().await; - let mut manager = setup_manager(server.uri()); + let mut manager = setup_manager(server.uri().parse().unwrap()); let history = vec![HistoryEntry { time: Utc::now(), unit_id: DUMMY_AD_UNITS[0].ipfs, @@ -677,9 +673,7 @@ mod test { price: UnifiedNum::from_whole(0.0001), }], }; - let res = manager - .get_sticky_ad_unit(&[campaign], "http://localhost:1337") - .await; + let res = manager.get_sticky_ad_unit(&[campaign], "localhost").await; assert!(res.is_some()); @@ -692,9 +686,7 @@ mod test { }], }; - let res = manager - .get_sticky_ad_unit(&[campaign], "http://localhost:1337") - .await; + let res = manager.get_sticky_ad_unit(&[campaign], "localhost").await; assert!(res.is_none()); } diff --git a/primitives/src/test_util.rs b/primitives/src/test_util.rs index c98478358..65b11481c 100644 --- a/primitives/src/test_util.rs +++ b/primitives/src/test_util.rs @@ -1,4 +1,7 @@ -use std::{collections::HashMap, ops::Deref}; +use std::{ + collections::{HashMap, HashSet}, + ops::Deref, +}; use chrono::{TimeZone, Utc}; use once_cell::sync::Lazy; @@ -500,3 +503,13 @@ pub static CAMPAIGNS: Lazy<[ChainOf; 3]> = Lazy::new(|| { [campaign_1337_1, campaign_1337_2, campaign_1_1] }); + +/// All the configured tokens in the `ganache.toml` config file +/// in all chains. +pub static WHITELISTED_TOKENS: Lazy> = Lazy::new(|| { + GANACHE_CONFIG + .chains + .values() + .flat_map(|chain| chain.tokens.values().map(|token| token.address)) + .collect() +}); From 600d55580acac1604df7175edf239f3416c711c3 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Tue, 1 Nov 2022 14:45:39 +0200 Subject: [PATCH 15/15] primitives - ad_slot example assertion Signed-off-by: Lachezar Lechev --- primitives/Cargo.toml | 4 ++ primitives/examples/ad_slot.rs | 84 +++++++++++++--------------------- 2 files changed, 37 insertions(+), 51 deletions(-) diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 4363ccc87..d3de91888 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -23,6 +23,10 @@ test-util = [] [[example]] name = "accounting_response" +[[example]] +name = "ad_slot" +required-features = ["test-util"] + [[example]] name = "all_spenders_response" diff --git a/primitives/examples/ad_slot.rs b/primitives/examples/ad_slot.rs index c846542df..dff87faf0 100644 --- a/primitives/examples/ad_slot.rs +++ b/primitives/examples/ad_slot.rs @@ -1,66 +1,48 @@ -use primitives::AdSlot; +use chrono::{TimeZone, Utc}; +use primitives::{ + targeting::Rules, + test_util::{DUMMY_AD_UNITS, DUMMY_IPFS, IDS, PUBLISHER}, + AdSlot, +}; use serde_json::{from_value, json}; fn main() { let json = json!({ "ipfs": "QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR", - "type": "legacy_250x250", - "minPerImpression": { - // `Mocked TOKEN 1: 0.1` - "0x12a28f2bfBFfDf5842657235cC058242f40fDEa6": "10000000" - }, + "type": "legacy_300x100", + "minPerImpression": null, "rules": [], - "fallbackUnit": null, + "fallbackUnit": "Qmasg8FrbuSQpjFu3kRnZF9beg8rEBFrqgi1uXDRwCbX5f", "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", // milliseconds - "created": "1564372800000", + "created": 1564372800000_u64, "title": "Test AdSlot", - "website": "adex.network", + "description": null, + "website": "https://adex.network", "archived": false, + // milliseconds + "modified": 1564372800000_u64 }); - // let ad_slot = AdSlot { - // ipfs: DUMMY_IPFS[0], - // ad_type: "legacy_250x250".to_string(), - // archived: false, - // created: Utc.ymd(2019, 7, 29).and_hms(7, 0, 0), - // description: Some("Test slot for running integration tests".to_string()), - // fallback_unit: Some(fallback_unit.ipfs), - // min_per_impression: Some( - // [ - // ( - // GANACHE_INFO_1.tokens["Mocked TOKEN 1"].address, - // UnifiedNum::from_whole(0.010), - // ), - // ( - // GANACHE_INFO_1337.tokens["Mocked TOKEN 1337"].address, - // UnifiedNum::from_whole(0.001), - // ), - // ] - // .into_iter() - // .collect(), - // ), - // modified: Some(Utc.ymd(2019, 7, 29).and_hms(7, 0, 0)), - // owner: IDS[&PUBLISHER], - // title: Some("Test slot 1".to_string()), - // website: Some("https://adex.network".to_string()), - // rules: Rules::default(), - // } + let fallback_unit = DUMMY_AD_UNITS[0].ipfs; - // let expected = AdSlot { - // ipfs: DUMMY_IPFS[0], - // ad_type: String, - // min_per_impression: Option>, - // rules: Rules::default(), - // fallback_unit: Some(), - // owner: ValidatorId, - // created: DateTime, - // title: Option, - // description: Option, - // website: Option, - // archived: false, - // modified: Option>, - // }; + let expected_ad_slot = AdSlot { + ipfs: DUMMY_IPFS[0], + ad_type: "legacy_300x100".to_string(), + min_per_impression: None, + rules: Rules::default(), + fallback_unit: Some(fallback_unit), + owner: IDS[&PUBLISHER], + created: Utc.ymd(2019, 7, 29).and_hms(4, 0, 0), + title: Some("Test AdSlot".to_string()), + description: None, + website: Some("https://adex.network".to_string()), + archived: false, + modified: Some(Utc.ymd(2019, 7, 29).and_hms(4, 0, 0)), + }; - assert!(from_value::(json).is_ok()); + pretty_assertions::assert_eq!( + from_value::(json).expect("Should deserialize"), + expected_ad_slot + ); }