From 799351f96f6ca506bbd0ac6c75b619d1872e2080 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 23 Jun 2022 12:07:19 +0200 Subject: [PATCH 1/5] sentry - application - fix docs --- sentry/src/application.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sentry/src/application.rs b/sentry/src/application.rs index 83cb9336c..5fa60f6bd 100644 --- a/sentry/src/application.rs +++ b/sentry/src/application.rs @@ -30,8 +30,8 @@ use hyper::{Body, Method, Request, Response}; use redis::aio::MultiplexedConnection; use slog::Logger; -/// an error used when deserializing a [`Config`] instance from environment variables -/// see [`Config::from_env()`] +/// an error used when deserializing a [`EnvConfig`] instance from environment variables +/// see [`EnvConfig::from_env()`] pub use envy::Error as EnvError; pub const DEFAULT_PORT: u16 = 8005; @@ -58,13 +58,13 @@ pub struct EnvConfig { /// Defaults to `0.0.0.0`: [`DEFAULT_IP_ADDR`] #[serde(default = "default_ip_addr")] pub ip_addr: IpAddr, - #[serde(deserialize_with = "redis_url", default = "default_redis_url")] /// Defaults to locally running Redis server: [`DEFAULT_REDIS_URL`] + #[serde(deserialize_with = "redis_url", default = "default_redis_url")] pub redis_url: ConnectionInfo, } impl EnvConfig { - /// Deserialize the application [`Config`] from Environment variables. + /// Deserialize the application [`EnvConfig`] from Environment variables. pub fn from_env() -> Result { envy::from_env() } From cbe5c78c703d14806c5c348b7bee2271df4da736 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 23 Jun 2022 12:07:37 +0200 Subject: [PATCH 2/5] sentry - examples - /campaign/list query --- sentry/examples/campaign_list_query.rs | 95 ++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 sentry/examples/campaign_list_query.rs diff --git a/sentry/examples/campaign_list_query.rs b/sentry/examples/campaign_list_query.rs new file mode 100644 index 000000000..ac319ae30 --- /dev/null +++ b/sentry/examples/campaign_list_query.rs @@ -0,0 +1,95 @@ +use chrono::{TimeZone, Utc}; +use primitives::{ + sentry::campaign_list::{CampaignListQuery, ValidatorParam}, + test_util::{ADVERTISER, FOLLOWER, LEADER}, +}; + +fn main() { + // Empty query - default values only + { + let empty_query = ""; + let query: CampaignListQuery = serde_qs::from_str(empty_query).unwrap(); + + assert_eq!(0, query.page); + assert!( + Utc::now() >= query.active_to_ge, + "By default `activeTo` is set to `Utc::now()`" + ); + assert!(query.creator.is_none()); + assert!(query.validator.is_none()); + } + + // Query with `activeTo` only + { + let active_to_query = "activeTo=1624192200"; + let active_to = CampaignListQuery { + page: 0, + active_to_ge: Utc.ymd(2021, 6, 20).and_hms(12, 30, 0), + creator: None, + validator: None, + }; + + assert_eq!(active_to, serde_qs::from_str(active_to_query).unwrap()); + } + + // Query with `page` & `activeTo` + { + let with_page_query = "page=14&activeTo=1624192200"; + let with_page = CampaignListQuery { + page: 14, + active_to_ge: Utc.ymd(2021, 6, 20).and_hms(12, 30, 0), + creator: None, + validator: None, + }; + + assert_eq!(with_page, serde_qs::from_str(with_page_query).unwrap()); + } + + // Query with `creator` + { + let with_creator_query = + "activeTo=1624192200&creator=0xDd589B43793934EF6Ad266067A0d1D4896b0dff0"; + + let with_creator = CampaignListQuery { + page: 0, + active_to_ge: Utc.ymd(2021, 6, 20).and_hms(12, 30, 0), + creator: Some(*ADVERTISER), + validator: None, + }; + + assert_eq!( + with_creator, + serde_qs::from_str(with_creator_query).unwrap() + ); + } + + // Query with `validator` + { + let with_creator_validator_query = "page=0&activeTo=1624192200&creator=0xDd589B43793934EF6Ad266067A0d1D4896b0dff0&validator=0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7"; + let with_creator_validator = CampaignListQuery { + page: 0, + active_to_ge: Utc.ymd(2021, 6, 20).and_hms(12, 30, 0), + creator: Some(*ADVERTISER), + validator: Some(ValidatorParam::Validator((*FOLLOWER).into())), + }; + + assert_eq!( + with_creator_validator, + serde_qs::from_str(with_creator_validator_query).unwrap() + ); + } + + // Query with `leader` + { + let with_leader_query = "page=0&activeTo=1624192200&creator=0xDd589B43793934EF6Ad266067A0d1D4896b0dff0&leader=0x80690751969B234697e9059e04ed72195c3507fa"; + + let with_leader = CampaignListQuery { + page: 0, + active_to_ge: Utc.ymd(2021, 6, 20).and_hms(12, 30, 0), + creator: Some(*ADVERTISER), + validator: Some(ValidatorParam::Leader((*LEADER).into())), + }; + + assert_eq!(with_leader, serde_qs::from_str(with_leader_query).unwrap()); + } +} From eec05004cabeaa6c665da49a75c5804823ff9590 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 23 Jun 2022 15:36:40 +0200 Subject: [PATCH 3/5] Sentry API docs: - Improve routing docs Add query examples for: - /v5/campaign/list - /v5/channel/list Improve other doc comments --- primitives/Cargo.toml | 8 + .../examples/campaign_list_query.rs | 34 +++- primitives/examples/channel_list_query.rs | 71 +++++++++ primitives/src/address.rs | 5 +- primitives/src/sentry.rs | 16 +- sentry/src/routes.rs | 148 +++++++++++++++--- sentry/src/routes/campaign.rs | 11 +- sentry/src/routes/channel.rs | 16 +- 8 files changed, 263 insertions(+), 46 deletions(-) rename {sentry => primitives}/examples/campaign_list_query.rs (64%) create mode 100644 primitives/examples/channel_list_query.rs diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 1907c4af2..52c096684 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -16,6 +16,14 @@ postgres = ["bytes", "tokio-postgres", "deadpool-postgres"] # All Addresses and keystore files exist in the ganache-cli setup for testing with the EthereumAdapter test-util = [] +[[example]] +name = "channel_list_query" +required-features = ["test-util"] + +[[example]] +name = "campaign_list_query" +required-features = ["test-util"] + [dependencies] # (De)Serialization serde = { version = "^1.0", features = ['derive'] } diff --git a/sentry/examples/campaign_list_query.rs b/primitives/examples/campaign_list_query.rs similarity index 64% rename from sentry/examples/campaign_list_query.rs rename to primitives/examples/campaign_list_query.rs index ac319ae30..2555fec72 100644 --- a/sentry/examples/campaign_list_query.rs +++ b/primitives/examples/campaign_list_query.rs @@ -1,7 +1,7 @@ use chrono::{TimeZone, Utc}; use primitives::{ sentry::campaign_list::{CampaignListQuery, ValidatorParam}, - test_util::{ADVERTISER, FOLLOWER, LEADER}, + test_util::{ADVERTISER, FOLLOWER, IDS, LEADER}, }; fn main() { @@ -19,6 +19,9 @@ fn main() { assert!(query.validator.is_none()); } + // In the following examples we always use `activeTo` + // as it makes simpler examples for assertions rather than using the default `Utc::now()` + // Query with `activeTo` only { let active_to_query = "activeTo=1624192200"; @@ -64,13 +67,15 @@ fn main() { } // Query with `validator` + // You can either have `leader` or `validator` but not both! { - let with_creator_validator_query = "page=0&activeTo=1624192200&creator=0xDd589B43793934EF6Ad266067A0d1D4896b0dff0&validator=0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7"; + let with_creator_validator_query = + "activeTo=1624192200&validator=0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7"; let with_creator_validator = CampaignListQuery { page: 0, active_to_ge: Utc.ymd(2021, 6, 20).and_hms(12, 30, 0), - creator: Some(*ADVERTISER), - validator: Some(ValidatorParam::Validator((*FOLLOWER).into())), + creator: None, + validator: Some(ValidatorParam::Validator(IDS[&FOLLOWER])), }; assert_eq!( @@ -80,16 +85,31 @@ fn main() { } // Query with `leader` + // You can either have `leader` or `validator` but not both! { - let with_leader_query = "page=0&activeTo=1624192200&creator=0xDd589B43793934EF6Ad266067A0d1D4896b0dff0&leader=0x80690751969B234697e9059e04ed72195c3507fa"; + let with_leader_query = "activeTo=1624192200&leader=0x80690751969B234697e9059e04ed72195c3507fa"; let with_leader = CampaignListQuery { page: 0, active_to_ge: Utc.ymd(2021, 6, 20).and_hms(12, 30, 0), - creator: Some(*ADVERTISER), - validator: Some(ValidatorParam::Leader((*LEADER).into())), + creator: None, + validator: Some(ValidatorParam::Leader(IDS[&LEADER])), }; assert_eq!(with_leader, serde_qs::from_str(with_leader_query).unwrap()); } + + // Query with all parameters and `validator` + // You can either have `leader` or `validator` but not both! + { + let full_query = "page=14&activeTo=1624192200&creator=0xDd589B43793934EF6Ad266067A0d1D4896b0dff0&validator=0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7"; + let full_expected = CampaignListQuery { + page: 14, + active_to_ge: Utc.ymd(2021, 6, 20).and_hms(12, 30, 0), + creator: Some(*ADVERTISER), + validator: Some(ValidatorParam::Validator(IDS[&FOLLOWER])), + }; + + assert_eq!(full_expected, serde_qs::from_str(full_query).unwrap()); + } } diff --git a/primitives/examples/channel_list_query.rs b/primitives/examples/channel_list_query.rs new file mode 100644 index 000000000..1e844ba80 --- /dev/null +++ b/primitives/examples/channel_list_query.rs @@ -0,0 +1,71 @@ +use primitives::{ + sentry::channel_list::ChannelListQuery, + test_util::{IDS, LEADER}, + ChainId, +}; + +fn main() { + // An empty query + { + let empty = ""; + let empty_expected = ChannelListQuery { + page: 0, + validator: None, + chains: vec![], + }; + + assert_eq!(empty_expected, serde_qs::from_str(empty).unwrap()); + } + + // Query with `page` + { + let only_page = "page=14"; + let only_page_expected = ChannelListQuery { + page: 14, + validator: None, + chains: vec![], + }; + + assert_eq!(only_page_expected, serde_qs::from_str(only_page).unwrap()); + } + + // Query with `validator` + { + let only_validator = "validator=0x80690751969B234697e9059e04ed72195c3507fa"; + let only_validator_expected = ChannelListQuery { + page: 0, + validator: Some(IDS[&LEADER]), + chains: vec![], + }; + + assert_eq!( + only_validator_expected, + serde_qs::from_str(only_validator).unwrap() + ); + } + + // Query with `chains` + { + let chains_query = "chains[]=1&chains[]=1337"; + let chains_expected = ChannelListQuery { + page: 0, + validator: None, + chains: vec![ChainId::new(1), ChainId::new(1337)], + }; + + assert_eq!(chains_expected, serde_qs::from_str(chains_query).unwrap()); + } + + // Query with all parameters + { + let all_query = + "page=14&validator=0x80690751969B234697e9059e04ed72195c3507fa&chains[]=1&chains[]=1337"; + let all_expected = ChannelListQuery { + page: 14, + validator: Some(IDS[&LEADER]), + chains: vec![ChainId::new(1), ChainId::new(1337)], + }; + + assert_eq!(all_expected, serde_qs::from_str(all_query).unwrap()); + } +} diff --git a/primitives/src/address.rs b/primitives/src/address.rs index f8e882af1..5d1eb1555 100644 --- a/primitives/src/address.rs +++ b/primitives/src/address.rs @@ -111,12 +111,13 @@ impl TryFrom<&str> for Address { } } -/// When we have a string literal (&str) representation of the Address in the form of bytes. +/// When we have a string literal (`&str`) representation of the `Address` in the form of bytes. /// Useful for creating static values from strings for testing, configuration, etc. /// -/// You can find a test setup example in the [`crate::test_util`] module. +/// You can find a test setup example in the `crate::test_util` module (requires `test-util` feature). /// /// # Example +/// /// ``` /// use once_cell::sync::Lazy; /// use primitives::Address; diff --git a/primitives/src/sentry.rs b/primitives/src/sentry.rs index 4574de5ee..d9c974d13 100644 --- a/primitives/src/sentry.rs +++ b/primitives/src/sentry.rs @@ -653,7 +653,14 @@ pub mod channel_list { pub pagination: Pagination, } - #[derive(Debug, Serialize, Deserialize)] + /// `GET /v5/channel/list` query + /// + /// # Examples + /// + /// ``` + #[doc = include_str!("../examples/channel_list_query.rs")] + /// ``` + #[derive(Debug, Serialize, Deserialize, PartialEq, Eq)] pub struct ChannelListQuery { /// default is `u64::default()` = `0` #[serde(default)] @@ -681,6 +688,13 @@ pub mod campaign_list { pub pagination: Pagination, } + /// `GET /v5/campaign/list` query + /// + /// # Examples + /// + /// ``` + #[doc = include_str!("../examples/campaign_list_query.rs")] + /// ``` #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct CampaignListQuery { /// default is `u64::default()` = `0` diff --git a/sentry/src/routes.rs b/sentry/src/routes.rs index 55749fb06..eba6f6335 100644 --- a/sentry/src/routes.rs +++ b/sentry/src/routes.rs @@ -1,5 +1,32 @@ //! Sentry REST API documentation //! +//! +//! All routes are listed below. Here is an overview and links to all of them: +//! - [Channel](#channel) routes +//! - [GET `/v5/channel/list`](#get-v5channellist) +//! - [GET `/v5/channel/:id/accounting`](#get-v5channelidaccounting) +//! - [GET `/v5/channel/:id/spender/:addr`](#get-v5channelidspenderaddr-auth-required) (auth required) +//! - [POST `/v5/channel/:id/spender/:addr`](#post-v5channelidspenderaddr-auth-required) (auth required) +//! - [GET `/v5/channel/:id/spender/all`](#get-v5channelidspenderall-auth-required) (auth required) +//! - [GET `/v5/channel/:id/validator-messages`](#get-v5channelidvalidator-messages) +//! - [GET `/v5/channel/:id/validator-messages/:addr`](#get-v5channelidvalidator-messages) +//! - [GET `/v5/channel/:id/validator-messages/:addr/:validator_messages`](#get-v5channelidvalidator-messages) +//! - [POST `/v5/channel/:id/validator-messages`](#post-v5channelidvalidator-messages-auth-required) (auth required) +//! - [GET `/v5/channel/:id/last-approved`](#get-v5channelidlast-approved) +//! - [POST `/v5/channel/:id/pay`](#post-v5channelidpay-auth-required) (auth required) +//! - [GET `/v5/channel/:id/get-leaf`](#get-v5channelidget-leaf) +//! - [Campaign](#campaign) routes +//! - [GET `/v5/campaign/list`](#get-v5campaignlist) +//! - [POST `/v5/campaign`](#post-v5campaign-auth-required) (auth required) +//! - [POST `/v5/campaign/:id`](#post-v5campaignid-auth-required) (auth required) +//! - [POST `/v5/campaign/:id/events`](#post-v5campaignidevents) (auth required) +//! - [POST `/v5/campaign/:id/close`](#post-v5campaignidclose-auth-required) (auth required) +//! - [Analytics](#analytics) routes +//! - [GET `/v5/analytics`](#get-v5analytics) +//! - [GET `/v5/analytics/for-publisher`](#get-v5analyticsfor-publisher-auth-required) (auth required) +//! - [GET `/v5/analytics/for-advertiser`](#get-v5analyticsfor-advertiser-auth-required) (auth required) +//! - [GET `/v5/analytics/for-admin`](#get-v5analyticsfor-admin-auth-required) (auth required) +//! //! ## Channel //! //! All routes are implemented under the module [channel]. @@ -13,48 +40,68 @@ //! //! ### Routes //! -//! - [`GET /v5/channel/list`](crate::routes::channel::channel_list) +//! #### GET `/v5/channel/list` +//! +//! The route is handled by [`channel::channel_list()`]. //! //! Request query parameters: [`ChannelListQuery`](primitives::sentry::channel_list::ChannelListQuery) //! //! Response: [`ChannelListResponse`](primitives::sentry::channel_list::ChannelListResponse) //! -//! - [`GET /v5/channel/:id/accounting`](channel::get_accounting_for_channel) +//! ##### Examples +//! +//! Query: +//! +//! ``` +#![doc = include_str!("../../primitives/examples/channel_list_query.rs")] +//! ``` +//! +//! #### GET `/v5/channel/:id/accounting` +//! +//! The route is handled by [`channel::get_accounting_for_channel()`]. //! //! Response: [`AccountingResponse::`](primitives::sentry::AccountingResponse) //! -//! - [`GET /v5/channel/:id/spender/:addr`](channel::get_spender_limits) (auth required) +//! #### GET `/v5/channel/:id/spender/:addr` (auth required) +//! +//! The route is handled by [`channel::get_spender_limits()`]. //! //! Response: [`SpenderResponse`](primitives::sentry::SpenderResponse) //! -//! - [`POST /v5/channel/:id/spender/:addr`](channel::add_spender_leaf) (auth required) +//! #### POST `/v5/channel/:id/spender/:addr` (auth required) //! //! This route forces the addition of a spender [`Accounting`] //! (if one does not exist) to the given [`Channel`] with `spent = 0`. //! This will also ensure that the spender is added to the [`NewState`] as well. //! +//! The route is handled by [`channel::add_spender_leaf()`]. +//! //! Response: [`SuccessResponse`] //! -//! - [`GET /v5/channel/:id/spender/all`](channel::get_all_spender_limits) (auth required) +//! #### GET `/v5/channel/:id/spender/all` (auth required) +//! +//! The route is handled by [`channel::get_all_spender_limits()`]. //! //! Response: [`AllSpendersResponse`](primitives::sentry::AllSpendersResponse) //! -//! - [`GET /v5/channel/:id/validator-messages`][list_validator_messages] +//! #### GET `/v5/channel/:id/validator-messages` +//! +//! - GET `/v5/channel/:id/validator-messages/:addr` - filter by the given [`ValidatorId`] +//! - GET `/v5/channel/:id/validator-messages/:addr/:validator_messages` - filters by the given [`ValidatorId`] and a +//! [`Validator message types`][`MessageTypes`]. +//! - `:validator_messages` - url encoded list of [`Validator message types`][`MessageTypes`] separated by a `+`. //! -//! - [`GET /v5/channel/:id/validator-messages/:addr`][list_validator_messages] - filter by the given [`ValidatorId`] -//! - [`GET /v5/channel/:id/validator-messages/:addr/:validator_messages`][list_validator_messages] - filters by the given [`ValidatorId`] and a -//! [`Validator message types`][`MessageTypes`]. -//! - `:validator_messages` - url encoded list of [`Validator message types`][`MessageTypes`] separated by a `+`. +//! E.g. `NewState+ApproveState` becomes `NewState%2BApproveState` //! -//! E.g. `NewState+ApproveState` becomes `NewState%2BApproveState` +//! The route is handled by [`channel::validator_message::list_validator_messages()`]. //! //! Request query parameters: [ValidatorMessagesListQuery](primitives::sentry::ValidatorMessagesListQuery) //! //! Response: [ValidatorMessagesListResponse](primitives::sentry::ValidatorMessagesListResponse) //! -//! [list_validator_messages]: channel::validator_message::list_validator_messages +//! #### POST `/v5/channel/:id/validator-messages` (auth required) //! -//! - [`POST /v5/channel/:id/validator-messages`](channel::validator_message::create_validator_messages) (auth required) +//! The route is handled by [`channel::validator_message::create_validator_messages()`]. //! //! Request body (json): [`ValidatorMessagesCreateRequest`](primitives::sentry::ValidatorMessagesCreateRequest) //! @@ -70,13 +117,15 @@ //! //! Validator messages: [`MessageTypes`] //! -//! - [`GET /v5/channel/:id/last-approved`](channel::last_approved) +//! #### GET `/v5/channel/:id/last-approved` +//! +//! The route is handled by [`channel::last_approved()`]. //! //! Request query parameters: [`LastApprovedQuery`][primitives::sentry::LastApprovedQuery] //! //! Response: [`LastApprovedResponse`][primitives::sentry::LastApprovedResponse] //! -//! - [`POST /v5/channel/:id/pay`](channel::channel_payout) (auth required) +//! #### POST `/v5/channel/:id/pay` (auth required) //! //! Channel Payout with authentication of the spender. //! @@ -85,12 +134,14 @@ //! all of the earners and updates their balances accordingly. Used when an advertiser/spender wants //! to get their remaining funds back. //! +//! The route is handled by [`channel::channel_payout()`]. +//! //! Request JSON body: [`ChannelPayRequest`](primitives::sentry::ChannelPayRequest) //! //! Response: [`SuccessResponse`](primitives::sentry::SuccessResponse) //! //! -//! - `GET /v5/channel/:id/get-leaf` +//! #### GET `/v5/channel/:id/get-leaf` //! //! TODO: implement and document as part of issue #382 //! @@ -98,6 +149,8 @@ //! and finds the given `spender`/`earner` in the balances tree, and produce a merkle proof for it. //! This is useful for the Platform to verify if a spender leaf really exists. //! +//! The route is handled by `todo`. +//! //! Request query parameters: //! //! - `spender=[0x...]` or `earner=[0x...]` (required) @@ -123,13 +176,15 @@ //! //! ### Routes //! -//! - [`GET /v5/campaign/list`](campaign::campaign_list) +//! #### GET `/v5/campaign/list` //! //! Lists all campaigns with pagination and orders them in -//! descending order (`DESC`) by `Campaign.created`. +//! ascending order (`ASC`) by `Campaign.created`. //! This ensures that the order in the pages will not change if a new //! `Campaign` is created while still retrieving a page. //! +//! The route is handled by [`campaign::campaign_list()`]. +//! //! Request query parameters: [`CampaignListQuery`][primitives::sentry::campaign_list::CampaignListQuery] //! //! - `page=[integer]` (optional) default: `0` @@ -139,9 +194,18 @@ //! - `validator=[0x...]` - it will return all `Campaign`s where this address is **either** `Channel.leader` or `Channel.follower` //! - `leader=[0x...]` - it will return all `Campaign`s where this address is `Channel.leader` //! +//! //! Response: [`CampaignListResponse`][primitives::sentry::campaign_list::CampaignListResponse] //! -//! - [`POST /v5/campaign`](campaign::create_campaign) (auth required) +//! ##### Examples +//! +//! Query: +//! +//! ``` +#![doc = include_str!("../../primitives/examples/campaign_list_query.rs")] +//! ``` +//! +//! #### POST `/v5/campaign` (auth required) //! //! Create a new Campaign. Request must be sent by the [`Campaign.creator`](primitives::Campaign::creator). //! @@ -150,25 +214,49 @@ //! It will make sure the `Channel` is created if new and it will update //! the spendable amount using the [`Adapter`]`::get_deposit()`. //! +//! The route is handled by [`campaign::create_campaign()`]. //! //! Request body (json): [`CreateCampaign`][primitives::sentry::campaign_create::CreateCampaign] //! //! Response: [`Campaign`] //! -//! - [`POST /v5/campaign/:id`](campaign::update_campaign::handle_route) (auth required) +//! #### POST `/v5/campaign/:id` (auth required) //! //! Modify the [`Campaign`]. Request must be sent by the [`Campaign.creator`](primitives::Campaign::creator). //! //! **Authentication is required** to validate [`Campaign.creator`](primitives::Campaign::creator) == [`Auth.uid`](crate::Auth::uid) //! +//! The route is handled by [`campaign::update_campaign::handle_route()`]. +//! //! Request body (json): [`ModifyCampaign`][primitives::sentry::campaign_modify::ModifyCampaign] //! //! Response: [`Campaign`] //! -//! - [`POST /v5/campaign/:id/close`](campaign::close_campaign) (auth required) +//! #### POST `/v5/campaign/:id/events` +//! +//! Add new [`Event`]s (`IMPRESSION`s & `CLICK`s) to the [`Campaign`]. +//! Applies [`Campaign.event_submission`] rules and additional validation using [`check_access()`]. +//! +//! The route is handled by [`campaign::insert_events::handle_route()`]. +//! +//! Request body (json): +//! +//! ```json +//! { +//! "events": [ +//! // Events +//! ] +//! } +//! ``` +//! +//! Response: [`SuccessResponse`] +//! +//! #### POST `/v5/campaign/:id/close` (auth required) //! //! Close the campaign. //! +//! The route is handled by [`campaign::close_campaign()`]. +//! //! Request must be sent by the [`Campaign.creator`](primitives::Campaign::creator). //! //! **Authentication is required** to validate [`Campaign.creator`](primitives::Campaign::creator) == [`Auth.uid`](crate::Auth::uid) @@ -179,30 +267,35 @@ //! //! ## Analytics //! -//! - [`GET /v5/analytics`](get_analytics) +//! #### GET `/v5/analytics` //! //! Allowed keys: [`AllowedKey::Country`][primitives::analytics::query::AllowedKey::Country], [`AllowedKey::AdSlotType`][primitives::analytics::query::AllowedKey::AdSlotType] //! -//! - [`GET /v5/analytics/for-publisher`](get_analytics) (auth required) +//! #### GET `/v5/analytics/for-publisher` (auth required) //! //! Returns all analytics where the currently authenticated address [`Auth.uid`](crate::Auth::uid) is a **publisher**. //! //! All [`ALLOWED_KEYS`] are allowed for this route. //! +//! The route is handled by [`get_analytics()`]. //! -//! - [`GET /v5/analytics/for-advertiser`](get_analytics) (auth required) +//! #### GET `/v5/analytics/for-advertiser` (auth required) //! //! Returns all analytics where the currently authenticated address [`Auth.uid`](crate::Auth::uid) is an **advertiser**. //! //! All [`ALLOWED_KEYS`] are allowed for this route. //! -//! - [`GET /v5/analytics/for-admin`](get_analytics) (auth required) +//! The route is handled by [`get_analytics()`]. +//! +//! #### GET `/v5/analytics/for-admin` (auth required) //! //! Admin access to the analytics with no restrictions on the keys for filtering. //! //! All [`ALLOWED_KEYS`] are allowed for admins. //! -//! Admin addresses are configured in the [`Config.admins`](primitives::Config::admins) +//! Admin addresses are configured in the [`Config.admins`](primitives::Config::admins). +//! +//! The route is handled by [`get_analytics()`]. //! //! [`Adapter`]: adapter::Adapter //! [`Address`]: primitives::Address @@ -217,6 +310,9 @@ //! [`Channel`]: primitives::Channel //! [`MessageTypes`]: primitives::validator::MessageTypes //! [`NewState`]: primitives::validator::NewState +//! [`Event`]: primitives::sentry::Event +//! [`Campaign.event_submission`]: primitives::Campaign::event_submission +//! [`check_access()`]: crate::access::check_access //! [`SuccessResponse`]: primitives::sentry::SuccessResponse //! [`ValidatorId`]: primitives::ValidatorId diff --git a/sentry/src/routes/campaign.rs b/sentry/src/routes/campaign.rs index a65bedd51..2757d590c 100644 --- a/sentry/src/routes/campaign.rs +++ b/sentry/src/routes/campaign.rs @@ -142,6 +142,7 @@ pub async fn fetch_campaign_ids_for_channel( } } +/// POST `/v5/campaign` pub async fn create_campaign( req: Request, app: &Application, @@ -289,6 +290,8 @@ where Ok(success_response(serde_json::to_string(&campaign)?)) } + +/// GET `/v5/campaign/list` pub async fn campaign_list( req: Request, app: &Application, @@ -313,8 +316,10 @@ pub async fn campaign_list( Ok(success_response(serde_json::to_string(&list_response)?)) } -/// Can only be called by creator -/// to close a campaign, just set it's budget to what it's spent so far (so that remaining == 0) +/// POST `/v5/campaign/:id/close` (auth required) +/// +/// Can only be called by the [`Campaign.creator`]! +/// To close a campaign, just set it's budget to what it's spent so far (so that remaining == 0) /// newBudget = totalSpent, i.e. newBudget = oldBudget - remaining pub async fn close_campaign( req: Request, @@ -364,6 +369,7 @@ pub mod update_campaign { use super::*; + /// POST `/v5/campaign/:id` (auth required) pub async fn handle_route( req: Request, app: &Application, @@ -623,6 +629,7 @@ pub mod insert_events { CampaignOutOfBudget, } + /// POST `/v5/campaign/:id` pub async fn handle_route( req: Request, app: &Application, diff --git a/sentry/src/routes/channel.rs b/sentry/src/routes/channel.rs index 1b6d03069..004bea2a4 100644 --- a/sentry/src/routes/channel.rs +++ b/sentry/src/routes/channel.rs @@ -32,7 +32,7 @@ use primitives::{ use slog::{error, Logger}; use std::{collections::HashMap, str::FromStr}; -/// `GET /v5/channel/list` request +/// GET `/v5/channel/list` request /// /// Query: [`ChannelListQuery`] /// @@ -59,7 +59,7 @@ pub async fn channel_list( Ok(success_response(serde_json::to_string(&list_response)?)) } -/// `GET /v5/channel/0xXXX.../last-approved` request +/// GET `/v5/channel/0xXXX.../last-approved` request /// /// Query: [`LastApprovedQuery`] /// @@ -179,7 +179,7 @@ fn spender_response_without_leaf( Ok(success_response(serde_json::to_string(&res)?)) } -/// `GET /v5/channel/0xXXX.../spender/0xXXX...` request +/// GET `/v5/channel/0xXXX.../spender/0xXXX...` request /// /// Response: [`SpenderResponse`] pub async fn get_spender_limits( @@ -236,7 +236,7 @@ pub async fn get_spender_limits( Ok(success_response(serde_json::to_string(&res)?)) } -/// `GET /v5/channel/0xXXX.../spender/all` request +/// GET `/v5/channel/0xXXX.../spender/all` request /// /// Response: [`AllSpendersResponse`] pub async fn get_all_spender_limits( @@ -293,9 +293,9 @@ pub async fn get_all_spender_limits( Ok(success_response(serde_json::to_string(&res)?)) } -/// `POST /v5/channel/0xXXX.../spender/0xXXX...` request +/// POST `/v5/channel/0xXXX.../spender/0xXXX...` request /// -/// internally, to make the validator worker to add a spender leaf in NewState we'll just update Accounting +/// Internally to make the validator worker add a spender leaf in `NewState` we'll just update `Accounting` pub async fn add_spender_leaf( req: Request, app: &Application, @@ -383,7 +383,7 @@ async fn get_corresponding_new_state( new_state } -/// `GET /v5/channel/0xXXX.../accounting` request +/// GET `/v5/channel/0xXXX.../accounting` request /// /// Response: [`AccountingResponse::`] pub async fn get_accounting_for_channel( @@ -425,7 +425,7 @@ pub async fn get_accounting_for_channel( Ok(success_response(serde_json::to_string(&res)?)) } -/// `POST /v5/channel/0xXXX.../pay` request +/// POST `/v5/channel/0xXXX.../pay` request /// /// Body: [`ChannelPayRequest`] /// From 8e6fc2f43803063395f7833a28ccdad996cc4ab1 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 23 Jun 2022 17:30:20 +0200 Subject: [PATCH 4/5] primitives - campaign_list_response example Add to sentry routes docs Add to CampaignListReponse --- primitives/Cargo.toml | 4 + primitives/examples/campaign_list_response.rs | 214 ++++++++++++++++++ primitives/src/sentry.rs | 16 +- sentry/src/routes.rs | 6 + 4 files changed, 238 insertions(+), 2 deletions(-) create mode 100644 primitives/examples/campaign_list_response.rs diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 52c096684..f11699152 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -24,6 +24,10 @@ required-features = ["test-util"] name = "campaign_list_query" required-features = ["test-util"] +[[example]] +name = "campaign_list_response" +required-features = ["test-util"] + [dependencies] # (De)Serialization serde = { version = "^1.0", features = ['derive'] } diff --git a/primitives/examples/campaign_list_response.rs b/primitives/examples/campaign_list_response.rs new file mode 100644 index 000000000..b8ef54dcc --- /dev/null +++ b/primitives/examples/campaign_list_response.rs @@ -0,0 +1,214 @@ +use primitives::sentry::campaign_list::CampaignListResponse; +use serde_json::{from_value, json}; + +fn main() { + let json = json!({ + "campaigns": [ + { + "id": "0x936da01f9abd4d9d80c702af85c822a8", + "channel": { + "leader": "0x80690751969B234697e9059e04ed72195c3507fa", + "follower": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7", + "guardian": "0xe061E1EB461EaBE512759aa18A201B20Fe90631D", + "token": "0x2BCaf6968aEC8A3b5126FBfAb5Fd419da6E8AD8E", + "nonce": "0" + }, + "creator": "0xDd589B43793934EF6Ad266067A0d1D4896b0dff0", + "budget": "15000000000", + "validators": [ + { + "id": "0x80690751969B234697e9059e04ed72195c3507fa", + "fee": "500000000", + "url": "http://localhost:8005/" + }, + { + "id": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7", + "fee": "400000000", + "url": "http://localhost:8006/" + } + ], + "title": "Dummy Campaign", + "pricingBounds": { + "CLICK": { + "min": "6000000", + "max": "10000000" + }, + "IMPRESSION": { + "min": "4000000", + "max": "5000000" + } + }, + "eventSubmission": { + "allow": [] + }, + "adUnits": [ + { + "ipfs": "Qmasg8FrbuSQpjFu3kRnZF9beg8rEBFrqgi1uXDRwCbX5f", + "type": "legacy_250x250", + "mediaUrl": "ipfs://QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR", + "mediaMime": "image/jpeg", + "targetUrl": "https://www.adex.network/?stremio-test-banner-1", + "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", + "created": 1564390800000_u64, + "title": "Dummy AdUnit 1", + "description": "Dummy AdUnit description 1", + "archived": false + }, + { + "ipfs": "QmVhRDGXoM3Fg3HZD5xwMuxtb9ZErwC8wHt8CjsfxaiUbZ", + "type": "legacy_250x250", + "mediaUrl": "ipfs://QmQB7uz7Gxfy7wqAnrnBcZFaVJLos8J9gn8mRcHQU6dAi1", + "mediaMime": "image/jpeg", + "targetUrl": "https://www.adex.network/?adex-campaign=true&pub=stremio", + "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", + "created": 1564390800000_u64, + "title": "Dummy AdUnit 2", + "description": "Dummy AdUnit description 2", + "archived": false + } + ], + "targetingRules": [], + "created": 1612162800000_u64, + "active_to": 4073414400000_u64 + }, + { + "id": "0x127b98248f4e4b73af409d10f62daeaa", + "channel": { + "leader": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7", + "follower": "0x80690751969B234697e9059e04ed72195c3507fa", + "guardian": "0x79D358a3194d737880B3eFD94ADccD246af9F535", + "token": "0x2BCaf6968aEC8A3b5126FBfAb5Fd419da6E8AD8E", + "nonce": "0" + }, + "creator": "0xDd589B43793934EF6Ad266067A0d1D4896b0dff0", + "budget": "2000000000", + "validators": [ + { + "id": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7", + "fee": "10000000", + "url": "http://localhost:8006/" + }, + { + "id": "0x80690751969B234697e9059e04ed72195c3507fa", + "fee": "5000000", + "url": "http://localhost:8005/" + } + ], + "title": "Dummy Campaign 2 in Chain #1337", + "pricingBounds": { + "CLICK": { + "min": "300000000", + "max": "500000000" + }, + "IMPRESSION": { + "min": "100000000", + "max": "200000000" + } + }, + "eventSubmission": { + "allow": [] + }, + "adUnits": [ + { + "ipfs": "Qmasg8FrbuSQpjFu3kRnZF9beg8rEBFrqgi1uXDRwCbX5f", + "type": "legacy_250x250", + "mediaUrl": "ipfs://QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR", + "mediaMime": "image/jpeg", + "targetUrl": "https://www.adex.network/?stremio-test-banner-1", + "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", + "created": 1564390800000_u64, + "title": "Dummy AdUnit 1", + "description": "Dummy AdUnit description 1", + "archived": false + }, + { + "ipfs": "QmVhRDGXoM3Fg3HZD5xwMuxtb9ZErwC8wHt8CjsfxaiUbZ", + "type": "legacy_250x250", + "mediaUrl": "ipfs://QmQB7uz7Gxfy7wqAnrnBcZFaVJLos8J9gn8mRcHQU6dAi1", + "mediaMime": "image/jpeg", + "targetUrl": "https://www.adex.network/?adex-campaign=true&pub=stremio", + "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", + "created": 1564390800000_u64, + "title": "Dummy AdUnit 2", + "description": "Dummy AdUnit description 2", + "archived": false + } + ], + "targetingRules": [], + "created": 1612162800000_u64, + "active_to": 4073414400000_u64 + }, + { + "id": "0xa78f3492481b41a688488a7aa1ff17df", + "channel": { + "leader": "0x80690751969B234697e9059e04ed72195c3507fa", + "follower": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7", + "guardian": "0x79D358a3194d737880B3eFD94ADccD246af9F535", + "token": "0x12a28f2bfBFfDf5842657235cC058242f40fDEa6", + "nonce": "1" + }, + "creator": "0x541b401362Ea1D489D322579552B099e801F3632", + "budget": "2000000000", + "validators": [ + { + "id": "0x80690751969B234697e9059e04ed72195c3507fa", + "fee": "200000000", + "url": "http://localhost:8005/" + }, + { + "id": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7", + "fee": "175000000", + "url": "http://localhost:8006/" + } + ], + "title": "Dummy Campaign 3 in Chain #1", + "pricingBounds": { + "CLICK": { + "min": "3500000", + "max": "6500000" + }, + "IMPRESSION": { + "min": "1500000", + "max": "2500000" + } + }, + "eventSubmission": { + "allow": [] + }, + "adUnits": [ + { + "ipfs": "QmYwcpMjmqJfo9ot1jGe9rfXsszFV1WbEA59QS7dEVHfJi", + "type": "legacy_250x250", + "mediaUrl": "ipfs://QmQB7uz7Gxfy7wqAnrnBcZFaVJLos8J9gn8mRcHQU6dAi1", + "mediaMime": "image/jpeg", + "targetUrl": "https://www.adex.network/?adex-campaign=true", + "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", + "created": 1564390800000_u64, + "title": "Dummy AdUnit 3", + "description": "Dummy AdUnit description 3", + "archived": false + }, + { + "ipfs": "QmTAF3FsFDS7Ru8WChoD9ofiHTH8gAQfR4mYSnwxqTDpJH", + "type": "legacy_250x250", + "mediaUrl": "ipfs://QmQAcfBJpDDuH99A4p3pFtUmQwamS8UYStP5HxHC7bgYXY", + "mediaMime": "image/jpeg", + "targetUrl": "https://adex.network", + "owner": "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9", + "created": 1564390800000_u64, + "title": "Dummy AdUnit 4", + "description": "Dummy AdUnit description 4", + "archived": false + } + ], + "targetingRules": [], + "created": 1612162800000_u64, + "active_to": 4073414400000_u64 + } + ], + "totalPages": 1, + "page": 0 + }); + + assert!(from_value::(json).is_ok()); +} diff --git a/primitives/src/sentry.rs b/primitives/src/sentry.rs index d9c974d13..90694cf1b 100644 --- a/primitives/src/sentry.rs +++ b/primitives/src/sentry.rs @@ -680,6 +680,13 @@ pub mod campaign_list { use super::Pagination; + /// `GET /v5/campaign/list` response + /// + /// # Examples + /// + /// ``` + #[doc = include_str!("../examples/campaign_list_response.rs")] + /// ``` #[derive(Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CampaignListResponse { @@ -697,19 +704,24 @@ pub mod campaign_list { /// ``` #[derive(Debug, Serialize, Deserialize, PartialEq)] pub struct CampaignListQuery { - /// default is `u64::default()` = `0` + /// Default is `u64::default()` = `0`. #[serde(default)] pub page: u64, - /// filters the list on `active.to >= active_to_ge` + /// Filters the list on `Campaign.active.to >= active_to_ge`. + /// /// It should be the same timestamp format as the `Campaign.active.to`: **seconds** + /// + /// **Note:** This field is deserialized from `activeTo`. #[serde(with = "ts_seconds", default = "Utc::now", rename = "activeTo")] pub active_to_ge: DateTime, + /// Returns only the [`Campaign`]s containing a specified creator if provided. pub creator: Option
, /// Returns only the [`Campaign`]s containing a specified validator if provided. #[serde(flatten)] pub validator: Option, } + /// The `validator` query parameter for [`CampaignListQuery`]. #[derive(Serialize, Deserialize, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub enum ValidatorParam { diff --git a/sentry/src/routes.rs b/sentry/src/routes.rs index eba6f6335..c4ede68b4 100644 --- a/sentry/src/routes.rs +++ b/sentry/src/routes.rs @@ -204,6 +204,12 @@ //! ``` #![doc = include_str!("../../primitives/examples/campaign_list_query.rs")] //! ``` +//! +//! Response: +//! +//! ``` +#![doc = include_str!("../../primitives/examples/campaign_list_response.rs")] +//! ``` //! //! #### POST `/v5/campaign` (auth required) //! From cafb86892e633b014de2a27d3c1e1f847a784b84 Mon Sep 17 00:00:00 2001 From: Lachezar Lechev Date: Thu, 23 Jun 2022 17:30:36 +0200 Subject: [PATCH 5/5] rustfmt --- primitives/examples/campaign_list_query.rs | 3 ++- sentry/src/routes.rs | 4 ++-- sentry/src/routes/campaign.rs | 1 - 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/primitives/examples/campaign_list_query.rs b/primitives/examples/campaign_list_query.rs index 2555fec72..62e3853f0 100644 --- a/primitives/examples/campaign_list_query.rs +++ b/primitives/examples/campaign_list_query.rs @@ -87,7 +87,8 @@ fn main() { // Query with `leader` // You can either have `leader` or `validator` but not both! { - let with_leader_query = "activeTo=1624192200&leader=0x80690751969B234697e9059e04ed72195c3507fa"; + let with_leader_query = + "activeTo=1624192200&leader=0x80690751969B234697e9059e04ed72195c3507fa"; let with_leader = CampaignListQuery { page: 0, diff --git a/sentry/src/routes.rs b/sentry/src/routes.rs index c4ede68b4..8892afe5c 100644 --- a/sentry/src/routes.rs +++ b/sentry/src/routes.rs @@ -204,9 +204,9 @@ //! ``` #![doc = include_str!("../../primitives/examples/campaign_list_query.rs")] //! ``` -//! +//! //! Response: -//! +//! //! ``` #![doc = include_str!("../../primitives/examples/campaign_list_response.rs")] //! ``` diff --git a/sentry/src/routes/campaign.rs b/sentry/src/routes/campaign.rs index 2757d590c..6e6952703 100644 --- a/sentry/src/routes/campaign.rs +++ b/sentry/src/routes/campaign.rs @@ -290,7 +290,6 @@ where Ok(success_response(serde_json::to_string(&campaign)?)) } - /// GET `/v5/campaign/list` pub async fn campaign_list( req: Request,