Module sentry::routes

source ·
Expand description

Sentry REST API documentation

This module includes the documentation for all routes of the Sentry REST API and the corresponding requests, responses and parameters.

All routes are listed below. Here is an overview and links to all of them:

Channel

All routes are implemented under the module channel.

Route parameters

Paths which include these parameters are validated as follows:

Routes

GET /v5/channel/list

The route is handled by channel::channel_list().

Request query parameters: ChannelListQuery

Response: ChannelListResponse

Examples

Query:

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());
    }
}
GET /v5/channel/:id/accounting

Gets all of the accounting entries for a channel from the database and checks the balances.

The route is handled by channel::get_accounting_for_channel().

Response: AccountingResponse

Examples

Response:

use primitives::{balances::CheckedState, sentry::AccountingResponse};
use serde_json::{from_value, json};

fn main() {
    // Empty balances
    {
        let json = json!({
          "earners": {},
          "spenders": {},
        });
        assert!(from_value::<AccountingResponse::<CheckedState>>(json).is_ok());
    }

    // Non-empty balances
    {
        // earners sum and spenders sum should always match since balances are CheckedState
        let json = json!({
            "earners": {
                "0x80690751969B234697e9059e04ed72195c3507fa": "10000000000",
                "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7": "20000000000",
                "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9": "30000000000",
            },
            "spenders": {
                "0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F": "60000000000",
            },
        });
        assert!(from_value::<AccountingResponse::<CheckedState>>(json).is_ok());
    }
}
GET /v5/channel/:id/spender/:addr (auth required)

Gets the spender limits for a spender on a Channel. It does so by fetching the latest Spendable entry from the database (or creating one if it doesn’t exist yet) from which the total deposited amount is retrieved, and the latest NewState from which the total spent amount is retrieved.

The route is handled by channel::get_spender_limits().

Response: SpenderResponse

Examples

Response:

use primitives::sentry::SpenderResponse;
use serde_json::{from_value, json};

fn main() {
    let json = json!({
        "spender": {
            "totalDeposited": "10000000000",
            "totalSpent": "100000000",
        },
    });
    assert!(from_value::<SpenderResponse>(json).is_ok());
}
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 (auth required)

This routes gets total_deposited and total_spent for every spender on a Channel

The route is handled by channel::get_all_spender_limits().

Response: AllSpendersResponse

Examples

Response:

use primitives::sentry::AllSpendersResponse;
use serde_json::{from_value, json};

fn main() {
    let json = json!({
        "spenders": {
            "0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F": {
                "totalDeposited": "10000000000",
                "totalSpent": "100000000",
            },
            "0xDd589B43793934EF6Ad266067A0d1D4896b0dff0": {
                "totalDeposited": "90000000000",
                "totalSpent": "20000000000",
            },
            "0x541b401362Ea1D489D322579552B099e801F3632": {
                "totalDeposited": "1000000000",
                "totalSpent": "1000000000",
            },
        },
        "totalPages": 1,
        "page": 0
    });
    assert!(from_value::<AllSpendersResponse>(json).is_ok());
}
GET /v5/channel/:id/validator-messages

Retrieve the latest validator MessageTypes for a given Channel.

The query limit parameter is constraint to a maximum of Config.limits.msgs_find, if a large value is passed it will use the Config.limits.msgs_find instead.

Sub-routes with additional filtering:

  • 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 validator MessageTypes.
    • :validator_messages - url encoded list of Validator MessageTypes separated by a +.

      E.g. NewState+ApproveState becomes NewState%2BApproveState

The route is handled by channel::validator_message::list_validator_messages().

Request query parameters: ValidatorMessagesListQuery

Response: ValidatorMessagesListResponse

Examples

Query:

use primitives::sentry::validator_messages::ValidatorMessagesListQuery;

fn main() {
    // Empty query - default values only
    {
        let empty_query = "";
        let query: ValidatorMessagesListQuery = serde_qs::from_str(empty_query).unwrap();

        assert_eq!(None, query.limit);
    }
    // Query with set limit
    {
        let query_str = "limit=200";
        let query: ValidatorMessagesListQuery = serde_qs::from_str(query_str).unwrap();

        assert_eq!(Some(200), query.limit);
    }
}

Response:

use primitives::sentry::validator_messages::ValidatorMessagesListResponse;
use serde_json::{from_value, json};

fn main() {
    // This response is generated using the Ethereum Adapter
    let response_json = json!({
        "messages": [
            {
                "from": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                "received": "2022-08-09T14:45:43.110Z",
                "msg": {
                    "type": "ApproveState",
                    "stateRoot": "a1e2f6ee08185ae06e3212e56ad1e0fcbae95ac8939871eb96e1ee3016234321",
                    "signature": "0xb2ce0010ad5867a4fb4acbde6525c261d76b592d290cb22af120573565168a2e49381e84d4f409c0989fa171dd687bf68b7eeff5b595c845cec8e9b8b1738dbd1c",
                    "isHealthy": true
                }
            },
            {
                "from": "0x80690751969B234697e9059e04ed72195c3507fa",
                "received": "2022-08-09T14:45:38.090Z",
                "msg": {
                    "type": "NewState",
                    "stateRoot": "a1e2f6ee08185ae06e3212e56ad1e0fcbae95ac8939871eb96e1ee3016234321",
                    "signature": "0x508bef21e91d5337ad71791503748fe0d7ee7592db90179be6f948570290d00b72e103b0d262452809ace183ebf83375072b4a359b6e441f2ad6f58b8552c8fa1b",
                    "earners": {
                        "0x80690751969B234697e9059e04ed72195c3507fa": "5",
                        "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9": "100000",
                        "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7": "3"
                    },
                    "spenders": {
                     "0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F": "100008"
                    }
                }
            },
            {
                "from": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                "received": "2022-08-09T14:45:28.160Z",
                "msg": {
                    "type": "Heartbeat",
                    "signature": "0x3afda200de4ac36d5c8f1a53da0ffdca5077b556a53fb56bb9a79def1c06f972547b0099731b1ac9b4a26c183e2ea66b8cd1759cdc1513e3436d182e9592ae0e1b",
                    "stateRoot": "fa8f11b8aa6322905846f96219c855920b4449b18f0ceea97552e3880c5e4a9a",
                    "timestamp": "2022-08-09T14:45:28.121225273Z"
                }
            }
        ]
    });

    let response: ValidatorMessagesListResponse =
        from_value(response_json).expect("Should deserialize");

    println!("{response:#?}");
}
POST /v5/channel/:id/validator-messages (auth required)

Create new validator MessageTypes for the given Channel, used when propagating messages from the validator worker.

Authentication is required to validate that the Auth.uid is either the Channel.leader or Channel.follower.

The route is handled by channel::validator_message::create_validator_messages().

Request body (json): ValidatorMessagesCreateRequest

Response: SuccessResponse

Examples

Request:

use primitives::sentry::validator_messages::ValidatorMessagesCreateRequest;
use serde_json::{from_value, json};

fn main() {
    // This request is generated using the Ethereum Adapter
    let request_json = json!({
        "messages": [
            {
                "type": "ApproveState",
                "stateRoot": "a1e2f6ee08185ae06e3212e56ad1e0fcbae95ac8939871eb96e1ee3016234321",
                "signature": "0xb2ce0010ad5867a4fb4acbde6525c261d76b592d290cb22af120573565168a2e49381e84d4f409c0989fa171dd687bf68b7eeff5b595c845cec8e9b8b1738dbd1c",
                "isHealthy": true
            },
            {
                "type": "NewState",
                "stateRoot": "a1e2f6ee08185ae06e3212e56ad1e0fcbae95ac8939871eb96e1ee3016234321",
                "signature": "0x508bef21e91d5337ad71791503748fe0d7ee7592db90179be6f948570290d00b72e103b0d262452809ace183ebf83375072b4a359b6e441f2ad6f58b8552c8fa1b",
                "earners": {
                    "0x80690751969B234697e9059e04ed72195c3507fa": "5",
                    "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9": "100000",
                    "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7": "3"
                },
                "spenders": {
                    "0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F": "100008"
                }
            },
            {
                "type": "Heartbeat",
                "signature": "0x3afda200de4ac36d5c8f1a53da0ffdca5077b556a53fb56bb9a79def1c06f972547b0099731b1ac9b4a26c183e2ea66b8cd1759cdc1513e3436d182e9592ae0e1b",
                "stateRoot": "fa8f11b8aa6322905846f96219c855920b4449b18f0ceea97552e3880c5e4a9a",
                "timestamp": "2022-08-09T14:45:28.121225273Z"
            }
        ]
    });

    let request: ValidatorMessagesCreateRequest =
        from_value(request_json).expect("Should deserialize");

    println!("{request:#?}");
}
GET /v5/channel/:id/last-approved

Retrieves the latest ApproveState and the corresponding NewState validator messages for the given Channel.

If the Channel is new one or both of the states might have not been generated yet.

The same is true of the Heartbeats messages if they are requested with the query parameter.

Retrieves the latest ApproveState and the corresponding NewState validator messages for the given Channel.

If the Channel is new one or both of the states might have not been generated yet.

The same is true of the Heartbeats messages if they are requested with the query parameter.

The route is handled by channel::last_approved().

Request query parameters: [LastApprovedQuery][primitives::sentry::LastApprovedQuery]

Response: [LastApprovedResponse][primitives::sentry::LastApprovedResponse]

Examples

Query:

use primitives::sentry::LastApprovedQuery;

fn main() {
    // An empty query - no heartbeats will be included in the response (default).
    {
        // This is treated the same as `withHeartbeat=false` in the route.
        let empty = "";
        let empty_expected = LastApprovedQuery {
            with_heartbeat: None,
        };

        assert_eq!(empty_expected, serde_qs::from_str(empty).unwrap());
    }

    // Query with `with_heartbeat` parameter - latest 2 Heartbeats from
    // each Channel Validator will be returned in the response.
    {
        let with_heartbeat = "withHeartbeat=true";
        let with_heartbeat_expected = LastApprovedQuery {
            with_heartbeat: Some(true),
        };

        assert_eq!(
            with_heartbeat_expected,
            serde_qs::from_str(with_heartbeat).unwrap()
        );
    }
}

Response:

use primitives::{balances::UncheckedState, sentry::LastApprovedResponse};
use serde_json::{from_value, json};

fn main() {
    // An empty response - Channel is brand new and neither an NewState nor a ApproveState
    // have been generated yet.
    // This is threated the same as `with_heartbeat=false` in the route.
    {
        let empty_json = json!({});

        let empty_expected = LastApprovedResponse::<UncheckedState> {
            last_approved: None,
            heartbeats: None,
        };

        assert_eq!(
            empty_expected,
            from_value(empty_json).expect("Should deserialize")
        );
    }

    // Response without `with_heartbeat=true` query parameter
    // or with a `with_heartbeat=false` query parameter
    {
        let response_json = json!({
            "lastApproved": {
                "newState": {
                    "from": "0x80690751969B234697e9059e04ed72195c3507fa",
                    "received": "2022-08-09T14:45:38.090Z",
                    "msg": {
                        "type": "NewState",
                        "stateRoot": "a1e2f6ee08185ae06e3212e56ad1e0fcbae95ac8939871eb96e1ee3016234321",
                        "signature": "0x508bef21e91d5337ad71791503748fe0d7ee7592db90179be6f948570290d00b72e103b0d262452809ace183ebf83375072b4a359b6e441f2ad6f58b8552c8fa1b",
                        "earners": {
                            "0x80690751969B234697e9059e04ed72195c3507fa": "5",
                            "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9": "100000",
                            "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7": "3"
                        },
                        "spenders": {
                            "0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F": "100008"
                        }
                    }
                },
                "approveState": {
                    "from": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                    "received": "2022-08-09T14:45:43.110Z",
                    "msg": {
                        "type": "ApproveState",
                        "stateRoot": "a1e2f6ee08185ae06e3212e56ad1e0fcbae95ac8939871eb96e1ee3016234321",
                        "signature": "0xb2ce0010ad5867a4fb4acbde6525c261d76b592d290cb22af120573565168a2e49381e84d4f409c0989fa171dd687bf68b7eeff5b595c845cec8e9b8b1738dbd1c",
                        "isHealthy": true
                    }
                }
            }
        });

        let response: LastApprovedResponse<UncheckedState> =
            from_value(response_json).expect("Should deserialize");
        assert!(response.heartbeats.is_none());
    }

    {
        let response_json = json!({
            "lastApproved": {
                "newState": {
                    "from": "0x80690751969B234697e9059e04ed72195c3507fa",
                    "received": "2022-08-09T14:45:38.090Z",
                    "msg": {
                        "type": "NewState",
                        "stateRoot": "a1e2f6ee08185ae06e3212e56ad1e0fcbae95ac8939871eb96e1ee3016234321",
                        "signature": "0x508bef21e91d5337ad71791503748fe0d7ee7592db90179be6f948570290d00b72e103b0d262452809ace183ebf83375072b4a359b6e441f2ad6f58b8552c8fa1b",
                        "earners": {
                            "0x80690751969B234697e9059e04ed72195c3507fa": "5",
                            "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9": "100000",
                            "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7": "3"
                        },
                        "spenders": {
                            "0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F": "100008"
                        }
                    }
                },
                "approveState": {
                    "from": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                    "received": "2022-08-09T14:45:43.110Z",
                    "msg": {
                        "type": "ApproveState",
                        "stateRoot": "a1e2f6ee08185ae06e3212e56ad1e0fcbae95ac8939871eb96e1ee3016234321",
                        "signature": "0xb2ce0010ad5867a4fb4acbde6525c261d76b592d290cb22af120573565168a2e49381e84d4f409c0989fa171dd687bf68b7eeff5b595c845cec8e9b8b1738dbd1c",
                        "isHealthy": true
                    }
                }
            },
            "heartbeats": [
                {
                    "from": "0x80690751969B234697e9059e04ed72195c3507fa",
                    "received": "2022-08-09T14:45:58.110Z",
                    "msg": {
                        "type": "Heartbeat",
                        "signature": "0xc96ed701fc53e27cb28b323c26060273a5bdbad07c4b5470d8090c6a8b03954422b5d0dcc791cc47b5e4186045f60971d7e8e4d69b380cf250fe416cf4aac6901b",
                        "stateRoot": "888f915d6b84ccfa80d3cc6536021efab05f63293ddfb66f1bb1c191909d1372",
                        "timestamp": "2022-08-09T14:45:58.097376454Z"
                    }
                },
                {
                    "from": "0x80690751969B234697e9059e04ed72195c3507fa",
                    "received": "2022-08-09T14:45:28.090Z",
                    "msg": {
                        "type": "Heartbeat",
                        "signature": "0x3f780e2abe0d704428a7921c2f18c070ad503953ef248f533b4ad13fa97c239c5e43a9f3db5077b24d4912cb13367337dc4a6c26976a15811a728e316e7275c41c",
                        "stateRoot": "799c72322f9c35840c4bf41045df2623b33a97a5dfa3994022389ddf8930aac6",
                        "timestamp": "2022-08-09T14:45:28.084928288Z"
                    }
                },
                {
                    "from": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                    "received": "2022-08-09T14:46:03.170Z",
                    "msg": {
                        "type": "Heartbeat",
                        "signature": "0xd4ce3c8e1cc8ab690cddc2a6cd229311e91e13a53e59b1ec80d8f877afd241af2c86c5fade37be5057d36c8fc5e69d3222b49b98bf686ee00e73005cc280ebc41b",
                        "stateRoot": "d0dc740d3352cdd7da9b823aa4051830f9757fae66c553a88176ce0001e378fb",
                        "timestamp": "2022-08-09T14:46:03.127887835Z"
                    }
                },
                {
                    "from": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                    "received": "2022-08-09T14:45:28.160Z",
                    "msg": {
                        "type": "Heartbeat",
                        "signature": "0x3afda200de4ac36d5c8f1a53da0ffdca5077b556a53fb56bb9a79def1c06f972547b0099731b1ac9b4a26c183e2ea66b8cd1759cdc1513e3436d182e9592ae0e1b",
                        "stateRoot": "fa8f11b8aa6322905846f96219c855920b4449b18f0ceea97552e3880c5e4a9a",
                        "timestamp": "2022-08-09T14:45:28.121225273Z"
                    }
                }
            ]
        });

        let response: LastApprovedResponse<UncheckedState> =
            from_value(response_json).expect("Should deserialize");

        assert!(response.heartbeats.is_some());
    }
}
POST /v5/channel/:id/pay (auth required)

Channel Payout with authentication of the spender.

This route handles withdrawals of advertiser funds for the authenticated spender. It needs to ensure all campaigns are closed. It accepts a JSON body in the request which contains 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

Response: SuccessResponse

Examples

Request (json):

use primitives::sentry::ChannelPayRequest;
use serde_json::json;

fn main() {
    let channel_pay_json = json!({
      "payouts": {
        "0x80690751969B234697e9059e04ed72195c3507fa": "10000000000",
        "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7": "20000000000",
        "0x0e880972A4b216906F05D67EeaaF55d16B5EE4F1": "30000000000",
      },
    });

    let channel_pay_json = serde_json::to_string(&channel_pay_json).expect("should serialize");

    assert!(serde_json::from_str::<ChannelPayRequest>(&channel_pay_json).is_ok());
}
GET `/v5/channel/:id/get-leaf

This route gets the latest approved state (NewState/ApproveState pair), finds the given spender or earner in the balances tree and produces a Merkle proof for it. This is useful for the Platform to verify if a spender leaf really exists.

The route is handled by channel::get_leaf().

Response: GetLeafResponse

Routes:
  • GET /v5/channel/:id/get-leaf/spender/:addr
  • GET /v5/channel/:id/get-leaf/earner/:addr
Examples:

URI for retrieving leaf of a Spender:

/v5/channel/0xf147fa3f1c5e5e06d359c15aa082442cc3e0380f306306022d1e9047c565a0f9/get-leaf/spender/0xDd589B43793934EF6Ad266067A0d1D4896b0dff0

URI for retrieving leaf of an Earner:

/v5/channel/0xf147fa3f1c5e5e06d359c15aa082442cc3e0380f306306022d1e9047c565a0f9/get-leaf/earner/0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9

Response:

use primitives::sentry::GetLeafResponse;
use serde_json::{from_value, json};

fn main() {
    let json = json!({
        "merkleProof": "8ea7760ca2dbbe00673372afbf8b05048717ce8a305f1f853afac8c244182e0c",
    });

    assert!(from_value::<GetLeafResponse>(json).is_ok());
}
POST /v5/channel/dummy-deposit (auth required)

Set a deposit for a Channel and depositor (the authenticated address) in the Dummy adapter.

NOTE: This route is available only when using the Dummy adapter and it’s not an official production route!

The route is handled by channel::channel_dummy_deposit().

Request body (json): ChannelDummyDeposit

Response: SuccessResponse

Examples

Request:

use primitives::{unified_num::FromWhole, UnifiedNum};
use sentry::routes::channel::ChannelDummyDeposit;
use serde_json::{from_value, json};

fn main() {
    let request_json = json!({
        "channel": {
            "leader": "0x80690751969B234697e9059e04ed72195c3507fa",
            "follower": "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
            "guardian": "0xe061E1EB461EaBE512759aa18A201B20Fe90631D",
            "token": "0x2bcaf6968aec8a3b5126fbfab5fd419da6e8ad8e",
            "nonce": "0"
          },
        "deposit": {
            "total": "20000000000000"
        }
    });

    let request: ChannelDummyDeposit = from_value(request_json).expect("Should deserialize");

    assert_eq!(UnifiedNum::from_whole(200000.0), request.deposit.total);

    println!("{request:#?}");
}

Campaign

All routes are implemented under the module campaign.

Route parameters

Paths which include these parameters are validated as follow:

Routes

GET /v5/campaign/list

Lists all campaigns with pagination and orders them in 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

  • page=[integer] (optional) default: 0
  • creator=[0x....] (optional) - address of the creator to be filtered by
  • activeTo=[integer] (optional) in seconds - filters campaigns by Campaign.active.to > query.activeTo
  • validator=[0x...] or leader=[0x...] (optional) - address of the validator to be filtered by. You can either
    • validator=[0x...] - it will return all Campaigns where this address is either Channel.leader or Channel.follower
    • leader=[0x...] - it will return all Campaigns where this address is Channel.leader

Response: CampaignListResponse

Examples

Query:

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());
    }
}

Response:

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": "6000",
              "max": "10000"
            },
            "IMPRESSION": {
              "min": "4000",
              "max": "5000"
            }
          },
          "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,
          "activeTo": 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": "300000",
              "max": "500000"
            },
            "IMPRESSION": {
              "min": "100000",
              "max": "200000"
            }
          },
          "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,
          "activeFrom": 1656280800000_u64,
          "activeTo": 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": "3500",
              "max": "6500"
            },
            "IMPRESSION": {
              "min": "1500",
              "max": "2500"
            }
          },
          "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,
          "activeTo": 4073414400000_u64
        }
      ],
      "totalPages": 1,
      "page": 0
    });

    assert!(from_value::<CampaignListResponse>(json).is_ok());
}
POST /v5/campaign (auth required)

Create a new Campaign. Request must be sent by the Campaign.creator.

Authentication is required to validate Campaign.creator == Auth.uid

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

Examples
use primitives::{sentry::campaign_create::CreateCampaign, test_util::DUMMY_CAMPAIGN, CampaignId};
use serde_json::json;
use std::str::FromStr;

fn main() {
    // CreateCampaign in an HTTP request.
    // A CampaignId will be randomly generated for the newly created Campaign.
    {
        let create_campaign = CreateCampaign::from_campaign_erased(DUMMY_CAMPAIGN.clone(), None);

        let _create_campaign_str =
            serde_json::to_string(&create_campaign).expect("should serialize");

        let create_campaign_json = json!({
            "channel":{
                "leader":"0x80690751969B234697e9059e04ed72195c3507fa",
                "follower":"0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                "guardian":"0xe061E1EB461EaBE512759aa18A201B20Fe90631D",
                "token":"0x2BCaf6968aEC8A3b5126FBfAb5Fd419da6E8AD8E",
                "nonce":"987654321"
            },
            "creator":"0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F",
            "budget":"100000000000",
            "validators":[
                {
                    "id":"0x80690751969B234697e9059e04ed72195c3507fa",
                    "fee":"3000",
                    "url":"http://localhost:8005"
                },
                {
                    "id":"0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                    "fee":"2000",
                    "url":"http://localhost:8006"
                }
            ],
            "title":"Dummy Campaign",
            "pricingBounds":{
                "CLICK":{"min":"0","max":"0"},
                "IMPRESSION":{"min":"1","max":"10"}
            },
            "eventSubmission":{"allow":[]},
            "targetingRules":[],
            "created":1612162800000_u64,
            "activeTo":4073414400000_u64
        });

        let create_campaign_json =
            serde_json::to_string(&create_campaign_json).expect("should serialize");

        let deserialized: CreateCampaign =
            serde_json::from_str(&create_campaign_json).expect("should deserialize");

        assert_eq!(create_campaign, deserialized);
    }

    // CreateCampaign with a provided ID
    {
        let mut create_campaign =
            CreateCampaign::from_campaign_erased(DUMMY_CAMPAIGN.clone(), None);
        create_campaign.id = Some(
            CampaignId::from_str("0x936da01f9abd4d9d80c702af85c822a8").expect("Should be valid id"),
        );

        let create_campaign_json = json!({
            "id":"0x936da01f9abd4d9d80c702af85c822a8",
            "channel":{
                "leader":"0x80690751969B234697e9059e04ed72195c3507fa",
                "follower":"0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                "guardian":"0xe061E1EB461EaBE512759aa18A201B20Fe90631D",
                "token":"0x2BCaf6968aEC8A3b5126FBfAb5Fd419da6E8AD8E",
                "nonce":"987654321"
            },
            "creator":"0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F",
            "budget":"100000000000",
            "validators":[
                {
                    "id":"0x80690751969B234697e9059e04ed72195c3507fa",
                    "fee":"3000",
                    "url":"http://localhost:8005"
                },
                {
                    "id":"0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
                    "fee":"2000",
                    "url":"http://localhost:8006"
                }
            ],
            "title":"Dummy Campaign",
            "pricingBounds":{"CLICK":{"min":"0","max":"0"},"IMPRESSION":{"min":"1","max":"10"}},
            "eventSubmission":{"allow":[]},
            "targetingRules":[],
            "created":1612162800000_u64,
            "activeTo":4073414400000_u64
        });

        let create_campaign_json =
            serde_json::to_string(&create_campaign_json).expect("should serialize");
        let deserialized: CreateCampaign =
            serde_json::from_str(&create_campaign_json).expect("should deserialize");

        assert_eq!(create_campaign, deserialized);
    }
}
POST /v5/campaign/:id (auth required)

Modify the Campaign. Request must be sent by the Campaign.creator.

Authentication is required to validate Campaign.creator == Auth.uid

The route is handled by campaign::update_campaign::handle_route().

Request body (json): [ModifyCampaign][primitives::sentry::campaign_modify::ModifyCampaign]

Response: Campaign

Examples
use primitives::{sentry::campaign_modify::ModifyCampaign, unified_num::FromWhole, UnifiedNum};
use serde_json::json;

fn main() {
    {
        let modify_campaign = ModifyCampaign {
            ad_units: None,
            budget: Some(UnifiedNum::from_whole(100)),
            validators: None,
            title: None,
            pricing_bounds: None,
            event_submission: None,
            targeting_rules: None,
        };

        let modify_campaign_json = json!({
            "ad_units": null,
            "budget": "10000000000",
            "validators": null,
            "title": null,
            "pricing_bounds": null,
            "event_submission": null,
            "targeting_rules": null,
        });

        let modify_campaign_json =
            serde_json::to_string(&modify_campaign_json).expect("should serialize");
        let deserialized: ModifyCampaign =
            serde_json::from_str(&modify_campaign_json).expect("should deserialize");

        assert_eq!(modify_campaign, deserialized);
    }
}
POST /v5/campaign/:id/events

Add new Events (IMPRESSIONs & CLICKs) 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): InsertEventsRequest

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.

Authentication is required to validate Campaign.creator == Auth.uid

Closes the campaign by setting Campaign.budget so that remaining budget = 0.

Response: SuccessResponse

Analytics

GET /v5/analytics

Allowed keys: [AllowedKey::Country][primitives::analytics::query::AllowedKey::Country], [AllowedKey::AdSlotType][primitives::analytics::query::AllowedKey::AdSlotType]

Request query parameters: AnalyticsQuery

Response: AnalyticsResponse

Examples

Query:

use primitives::{
    analytics::{
        query::{AllowedKey, Time},
        AnalyticsQuery, Metric, OperatingSystem, Timeframe,
    },
    sentry::{DateHour, EventType},
    Address, CampaignId, ChainId, IPFS,
};
use std::str::FromStr;

fn main() {
    // Empty query - default values only
    {
        let empty_query = "";
        let query: AnalyticsQuery = serde_qs::from_str(empty_query).unwrap();

        assert_eq!(100, query.limit);
        assert_eq!(EventType::Impression, query.event_type);
        assert!(matches!(query.metric, Metric::Count));
        assert!(matches!(query.time.timeframe, Timeframe::Day));
    }
    // Query with different metric/chain/eventType
    {
        let query_str = "limit=200&eventType=CLICK&metric=paid&timeframe=month";
        let query: AnalyticsQuery = serde_qs::from_str(query_str).unwrap();

        assert_eq!(200, query.limit);
        assert_eq!(EventType::Click, query.event_type);
        assert!(matches!(query.metric, Metric::Paid));
        assert!(matches!(query.time.timeframe, Timeframe::Month));
    }

    // Query with allowed keys for guest - country, slotType
    {
        let query_str = "country=Bulgaria&adSlotType=legacy_300x100";
        let query: AnalyticsQuery = serde_qs::from_str(query_str).unwrap();

        assert_eq!(Some("Bulgaria".to_string()), query.country);
        assert_eq!(Some("legacy_300x100".to_string()), query.ad_slot_type);
    }

    // Query with all possible fields (publisher/advertiser/admin)
    {
        let query_str = "limit=200&eventType=CLICK&metric=paid&segmentBy=country\
        &timeframe=week&start=2022-08-04+09:00:00.000000000+UTC\
        &campaignId=0x936da01f9abd4d9d80c702af85c822a8\
        &adUnit=QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR\
        &adSlot=Qmasg8FrbuSQpjFu3kRnZF9beg8rEBFrqgi1uXDRwCbX5f\
        &adSlotType=legacy_300x100\
        &advertiser=0xDd589B43793934EF6Ad266067A0d1D4896b0dff0\
        &publisher=0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9\
        &hostname=localhost&country=Bulgaria&osName=Windows\
        &chains[0]=1&chains[1]=1337";
        let query: AnalyticsQuery = serde_qs::from_str(query_str).unwrap();

        assert_eq!(query.limit, 200);
        assert_eq!(query.event_type, EventType::Click);
        assert!(matches!(query.metric, Metric::Paid));
        assert_eq!(query.segment_by, Some(AllowedKey::Country));
        assert_eq!(
            query.time,
            Time {
                timeframe: Timeframe::Week,
                start: DateHour::from_ymdh(2022, 8, 4, 9),
                end: None,
            }
        );
        assert_eq!(
            query.campaign_id,
            Some(
                CampaignId::from_str("0x936da01f9abd4d9d80c702af85c822a8")
                    .expect("should be valid")
            )
        );
        assert_eq!(
            query.ad_unit,
            Some(
                IPFS::from_str("QmcUVX7fvoLMM93uN2bD3wGTH8MXSxeL8hojYfL2Lhp7mR")
                    .expect("should be valid")
            )
        );
        assert_eq!(
            query.ad_slot,
            Some(
                IPFS::from_str("Qmasg8FrbuSQpjFu3kRnZF9beg8rEBFrqgi1uXDRwCbX5f")
                    .expect("should be valid")
            )
        );
        assert_eq!(query.ad_slot_type, Some("legacy_300x100".to_string()));
        assert_eq!(
            query.advertiser,
            Some(
                Address::from_str("0xDd589B43793934EF6Ad266067A0d1D4896b0dff0")
                    .expect("should be valid")
            )
        );
        assert_eq!(
            query.publisher,
            Some(
                Address::from_str("0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9")
                    .expect("should be valid")
            )
        );
        assert_eq!(query.hostname, Some("localhost".to_string()));
        assert_eq!(query.country, Some("Bulgaria".to_string()));
        assert_eq!(
            query.os_name,
            Some(OperatingSystem::Whitelisted("Windows".to_string()))
        );
        assert_eq!(query.chains, vec!(ChainId::new(1), ChainId::new(1337)));
    }
}

Response:

use primitives::sentry::AnalyticsResponse;
use serde_json::{from_value, json};

fn main() {
    let json = json!({
      "analytics": [{
        "time": 1659592800,
        "value": "3",
        "segment": null
      },
      {
        "time": 1659592800,
        "value": "10000000000",
        "segment": null
      },
      {
        "time": 1659592800,
        "value": "100000000",
        "segment": "country"
      }]
    });

    assert!(from_value::<AnalyticsResponse>(json).is_ok());
}
GET /v5/analytics/for-publisher (auth required)

Returns all analytics where the currently authenticated address Auth.uid is a publisher.

All ALLOWED_KEYS are allowed for this route.

The route is handled by get_analytics().

Request query parameters: AnalyticsQuery

Response: AnalyticsResponse

Examples

See GET /v5/analytics

GET /v5/analytics/for-advertiser (auth required)

Returns all analytics where the currently authenticated address Auth.uid is an advertiser.

All ALLOWED_KEYS are allowed for this route.

The route is handled by get_analytics().

Request query parameters: AnalyticsQuery

Response: AnalyticsResponse

Examples

See GET /v5/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.sentry.admins.

The route is handled by get_analytics().

Request query parameters: AnalyticsQuery

Response: AnalyticsResponse

Examples

See GET /v5/analytics

GET /cfg

Gets the config that the validator is running on.

The route is handled by get_cfg()

Response: Config

Examples

Response:

use primitives::{config::GANACHE_CONFIG, Config};
use serde_json::{from_value, json};

fn main() {
    let json = json!({
      "creators_whitelist": [
        "0xaCBaDA2d5830d1875ae3D2de207A1363B316Df2F",
        "0xDd589B43793934EF6Ad266067A0d1D4896b0dff0",
        "0xE882ebF439207a70dDcCb39E13CA8506c9F45fD9",
        "0x541b401362Ea1D489D322579552B099e801F3632"
      ],
      "validators_whitelist": [
        "0x80690751969B234697e9059e04ed72195c3507fa",
        "0xf3f583AEC5f7C030722Fe992A5688557e1B86ef7",
        "0x6B83e7D6B72c098d48968441e0d05658dc17Adb9"
      ],
      "chain": {
        "Ganache #1337": {
          "chain_id": 1337,
          "rpc": "http://localhost:1337/",
          "outpace": "0xAbc27d46a458E2e49DaBfEf45ca74dEDBAc3DD06",
          "token": {
            "Mocked TOKEN 1337": {
              "min_campaign_budget": "1000000000000000000",
              "min_validator_fee": "1000000000000",
              "precision": 18,
              "address": "0x2BCaf6968aEC8A3b5126FBfAb5Fd419da6E8AD8E"
            }
          }
        },
        "Ganache #1": {
          "chain_id": 1,
          "rpc": "http://localhost:8545/",
          "outpace": "0x26CBc2eAAe377f6Ac4b73a982CD1125eF4CEC96f",
          "token": {
            "Mocked TOKEN 1": {
              "min_campaign_budget": "1000000000000000000",
              "min_validator_fee": "1000000000000",
              "precision": 18,
              "address": "0x12a28f2bfBFfDf5842657235cC058242f40fDEa6"
            }
          }
        }
      },
      "limits": {
        "units_for_slot": {
          "max_campaigns_earning_from": 25,
          "global_min_impression_price": "10"
        },
        "channels_find": 200,
        "campaigns_find": 200,
        "spendable_find": 200,
        "msgs_find": 10,
        "analytics_find": 5000,
        "ip_rate_limit": {
          "type": "ip",
          "timeframe": 1200000
        }
      },
      "sentry": {
        "analytics_maxtime": 20000,
        "admins": [
          "0x80690751969B234697e9059e04ed72195c3507fa"
        ],
        "platform": {
          "url": "https://platform.adex.network/",
          "keep_alive_interval": 1200000
        },
      },
      "worker": {
        "max_channels": 512,
        "wait_time": 500,
        "heartbeat_time": 30000,
        "health_threshold_promilles": 950,
        "health_unsignable_promilles": 750,
        "timeouts": {
          "propagation": 2000,
          "fetch": 5000,
          "all_campaigns": 5000,
          "channel_tick": 8000,
        }
      }
    });
    assert_eq!(
        from_value::<Config>(json).expect("Should deserialize"),
        GANACHE_CONFIG.clone()
    );
}

Modules

/v5/campaign routes
/v5/channel routes
This module contains all the Sentry REST API routers.

Functions

GET /v5/analytics routes Request query parameters: [AnalyticsQuery].
GET /cfg request