diff --git a/primitives/Cargo.toml b/primitives/Cargo.toml index 1907c4af2..f11699152 100644 --- a/primitives/Cargo.toml +++ b/primitives/Cargo.toml @@ -16,6 +16,18 @@ 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"] + +[[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_query.rs b/primitives/examples/campaign_list_query.rs new file mode 100644 index 000000000..62e3853f0 --- /dev/null +++ b/primitives/examples/campaign_list_query.rs @@ -0,0 +1,116 @@ +use chrono::{TimeZone, Utc}; +use primitives::{ + sentry::campaign_list::{CampaignListQuery, ValidatorParam}, + test_util::{ADVERTISER, FOLLOWER, IDS, 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()); + } + + // 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"; + 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` + // You can either have `leader` or `validator` but not both! + { + 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: None, + validator: Some(ValidatorParam::Validator(IDS[&FOLLOWER])), + }; + + assert_eq!( + with_creator_validator, + serde_qs::from_str(with_creator_validator_query).unwrap() + ); + } + + // Query with `leader` + // You can either have `leader` or `validator` but not both! + { + 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: 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/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/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..90694cf1b 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)] @@ -673,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 { @@ -681,21 +695,33 @@ 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` + /// 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/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() } diff --git a/sentry/src/routes.rs b/sentry/src/routes.rs index 55749fb06..8892afe5c 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,24 @@ //! - `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")] +//! ``` +//! +//! Response: +//! +//! ``` +#![doc = include_str!("../../primitives/examples/campaign_list_response.rs")] +//! ``` +//! +//! #### POST `/v5/campaign` (auth required) //! //! Create a new Campaign. Request must be sent by the [`Campaign.creator`](primitives::Campaign::creator). //! @@ -150,25 +220,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 +273,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 +316,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..6e6952703 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,7 @@ where Ok(success_response(serde_json::to_string(&campaign)?)) } +/// GET `/v5/campaign/list` pub async fn campaign_list( req: Request, app: &Application, @@ -313,8 +315,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 +368,7 @@ pub mod update_campaign { use super::*; + /// POST `/v5/campaign/:id` (auth required) pub async fn handle_route( req: Request, app: &Application, @@ -623,6 +628,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`] ///