Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cc5e12e2b5 | |||
| b31587e051 | |||
| 9b1574ff08 | |||
| d3d5e327c7 | |||
| 433604dec7 | |||
| c429e67d68 |
Generated
+3
@@ -3243,6 +3243,7 @@ dependencies = [
|
||||
"mixnet-contract-common",
|
||||
"nymcoconut",
|
||||
"nymsphinx",
|
||||
"okapi",
|
||||
"pin-project",
|
||||
"pretty_env_logger",
|
||||
"rand 0.7.3",
|
||||
@@ -3250,7 +3251,9 @@ dependencies = [
|
||||
"reqwest",
|
||||
"rocket",
|
||||
"rocket_cors",
|
||||
"rocket_okapi",
|
||||
"rocket_sync_db_pools",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmwasm_std::Env;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{InstanceType, Schema, SchemaObject};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryInto;
|
||||
use std::fmt::{Display, Formatter};
|
||||
@@ -64,6 +67,39 @@ pub struct Interval {
|
||||
length: Duration,
|
||||
}
|
||||
|
||||
impl JsonSchema for Interval {
|
||||
fn schema_name() -> String {
|
||||
"Interval".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
|
||||
let mut schema_object = SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
..SchemaObject::default()
|
||||
};
|
||||
|
||||
let object_validation = schema_object.object();
|
||||
object_validation
|
||||
.properties
|
||||
.insert("id".to_owned(), gen.subschema_for::<u32>());
|
||||
object_validation.required.insert("id".to_owned());
|
||||
|
||||
// PrimitiveDateTime does not implement JsonSchema. However it has a custom
|
||||
// serialization to string, so we just specify the schema to be String.
|
||||
object_validation
|
||||
.properties
|
||||
.insert("start".to_owned(), gen.subschema_for::<String>());
|
||||
object_validation.required.insert("start".to_owned());
|
||||
|
||||
object_validation
|
||||
.properties
|
||||
.insert("length".to_owned(), gen.subschema_for::<Duration>());
|
||||
object_validation.required.insert("length".to_owned());
|
||||
|
||||
Schema::Object(schema_object)
|
||||
}
|
||||
}
|
||||
|
||||
impl Interval {
|
||||
/// Initialize epoch in the contract with default values.
|
||||
pub fn init_epoch(env: Env) -> Self {
|
||||
|
||||
@@ -24,18 +24,18 @@ humantime-serde = "1.0"
|
||||
log = "0.4"
|
||||
pin-project = "1.0"
|
||||
pretty_env_logger = "0.4"
|
||||
rand-07 = { package = "rand", version = "0.7" } # required for compatibility
|
||||
rand = "0.8"
|
||||
rand-07 = { package = "rand", version = "0.7" } # required for compatibility
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
rocket = { version = "0.5.0-rc.1", features = ["json"] }
|
||||
rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
tokio = { version = "1.4", features = ["rt-multi-thread", "macros", "signal", "time"] }
|
||||
tokio-stream = "0.1.8"
|
||||
rocket_cors = { git="https://github.com/lawliet89/rocket_cors", rev="dfd3662c49e2f6fc37df35091cb94d82f7fb5915" }
|
||||
url = "2.2"
|
||||
thiserror = "1"
|
||||
time = { version = "0.3", features = ["serde-human-readable", "parsing"]}
|
||||
tokio = { version = "1.4", features = ["rt-multi-thread", "macros", "signal", "time"] }
|
||||
tokio-stream = "0.1.8"
|
||||
url = "2.2"
|
||||
|
||||
anyhow = "1"
|
||||
getset = "0.1.1"
|
||||
@@ -43,6 +43,9 @@ getset = "0.1.1"
|
||||
rocket_sync_db_pools = { version = "0.1.0-rc.1", default-features = false }
|
||||
sqlx = { version = "0.5", features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]}
|
||||
|
||||
okapi = { version = "0.7.0-rc.1", features = ["impl_json_schema"] }
|
||||
rocket_okapi = { version = "0.8.0-rc.1", features = ["swagger"] }
|
||||
schemars = { version = "0.8", features = ["preserve_order"] }
|
||||
|
||||
## internal
|
||||
coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" }
|
||||
|
||||
@@ -9,6 +9,7 @@ use mixnet_contract_common::reward_params::EpochRewardParams;
|
||||
use mixnet_contract_common::{
|
||||
GatewayBond, IdentityKey, IdentityKeyRef, Interval, MixNodeBond, RewardedSetNodeStatus,
|
||||
};
|
||||
use rocket_okapi::openapi_get_routes;
|
||||
|
||||
use rocket::fairing::AdHoc;
|
||||
use serde::Serialize;
|
||||
@@ -191,7 +192,7 @@ impl ValidatorCache {
|
||||
rocket.manage(Self::new()).mount(
|
||||
// this format! is so ugly...
|
||||
format!("/{}", VALIDATOR_API_VERSION),
|
||||
routes![
|
||||
openapi_get_routes![
|
||||
routes::get_mixnodes,
|
||||
routes::get_gateways,
|
||||
routes::get_active_set,
|
||||
|
||||
@@ -6,28 +6,34 @@ use mixnet_contract_common::reward_params::EpochRewardParams;
|
||||
use mixnet_contract_common::{GatewayBond, Interval, MixNodeBond};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes")]
|
||||
pub async fn get_mixnodes(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.mixnodes().await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/gateways")]
|
||||
pub async fn get_gateways(cache: &State<ValidatorCache>) -> Json<Vec<GatewayBond>> {
|
||||
Json(cache.gateways().await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes/rewarded")]
|
||||
pub async fn get_rewarded_set(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.rewarded_set().await.value)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes/active")]
|
||||
pub async fn get_active_set(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeBond>> {
|
||||
Json(cache.active_set().await.value)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes/blacklisted")]
|
||||
pub async fn get_blacklisted_mixnodes(
|
||||
cache: &State<ValidatorCache>,
|
||||
@@ -35,6 +41,7 @@ pub async fn get_blacklisted_mixnodes(
|
||||
Json(cache.mixnodes_blacklist().await.map(|c| c.value))
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/gateways/blacklisted")]
|
||||
pub async fn get_blacklisted_gateways(
|
||||
cache: &State<ValidatorCache>,
|
||||
@@ -42,11 +49,13 @@ pub async fn get_blacklisted_gateways(
|
||||
Json(cache.gateways_blacklist().await.map(|c| c.value))
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/epoch/reward_params")]
|
||||
pub async fn get_epoch_reward_params(cache: &State<ValidatorCache>) -> Json<EpochRewardParams> {
|
||||
Json(cache.epoch_reward_params().await.value)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/epoch/current")]
|
||||
pub async fn get_current_epoch(cache: &State<ValidatorCache>) -> Json<Option<Interval>> {
|
||||
Json(cache.current_epoch().await.value)
|
||||
|
||||
@@ -19,6 +19,8 @@ use rocket::fairing::AdHoc;
|
||||
use rocket::http::Method;
|
||||
use rocket::{Ignite, Rocket};
|
||||
use rocket_cors::{AllowedHeaders, AllowedOrigins, Cors};
|
||||
use rocket_okapi::settings::UrlObject;
|
||||
use rocket_okapi::swagger_ui::{make_swagger_ui, SwaggerUIConfig};
|
||||
use std::process;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
@@ -398,6 +400,7 @@ async fn setup_rocket(
|
||||
) -> Result<Rocket<Ignite>> {
|
||||
// let's build our rocket!
|
||||
let rocket = rocket::build()
|
||||
.mount("/swagger", make_swagger_ui(&get_docs()))
|
||||
.attach(setup_cors()?)
|
||||
.attach(setup_liftoff_notify(liftoff_notify))
|
||||
.attach(ValidatorCache::stage());
|
||||
@@ -436,6 +439,16 @@ async fn setup_rocket(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_docs() -> SwaggerUIConfig {
|
||||
SwaggerUIConfig {
|
||||
urls: vec![
|
||||
UrlObject::new("Contract cache", "../v1/openapi.json"),
|
||||
UrlObject::new("Node status", "../v1/status/openapi.json"),
|
||||
],
|
||||
..SwaggerUIConfig::default()
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> {
|
||||
let system_version = env!("CARGO_PKG_VERSION");
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use rocket::fairing::AdHoc;
|
||||
use rocket_okapi::openapi_get_routes;
|
||||
use std::time::Duration;
|
||||
|
||||
pub(crate) mod local_guard;
|
||||
@@ -18,7 +19,7 @@ pub(crate) fn stage_full() -> AdHoc {
|
||||
AdHoc::on_ignite("Node Status API Stage", |rocket| async {
|
||||
rocket.mount(
|
||||
"/v1/status",
|
||||
routes![
|
||||
openapi_get_routes![
|
||||
routes::mixnode_report,
|
||||
routes::gateway_report,
|
||||
routes::mixnode_uptime_history,
|
||||
@@ -40,7 +41,7 @@ pub(crate) fn stage_minimal() -> AdHoc {
|
||||
AdHoc::on_ignite("Node Status API Stage", |rocket| async {
|
||||
rocket.mount(
|
||||
"/v1/status",
|
||||
routes![
|
||||
openapi_get_routes![
|
||||
routes::get_mixnode_status,
|
||||
routes::get_mixnode_stake_saturation,
|
||||
routes::get_mixnode_inclusion_probability,
|
||||
|
||||
@@ -3,9 +3,16 @@
|
||||
|
||||
use crate::node_status_api::utils::NodeUptimes;
|
||||
use crate::storage::models::NodeStatus;
|
||||
use okapi::openapi3::{Responses, SchemaObject};
|
||||
use rocket::http::{ContentType, Status};
|
||||
use rocket::response::{self, Responder, Response};
|
||||
use rocket::Request;
|
||||
use rocket_okapi::gen::OpenApiGenerator;
|
||||
use rocket_okapi::response::OpenApiResponderInner;
|
||||
use rocket_okapi::util::ensure_status_code_exists;
|
||||
use schemars::gen::SchemaGenerator;
|
||||
use schemars::schema::{InstanceType, Schema};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{self, Display, Formatter};
|
||||
@@ -17,7 +24,7 @@ use time::OffsetDateTime;
|
||||
pub struct InvalidUptime;
|
||||
|
||||
// value in range 0-100
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, JsonSchema)]
|
||||
pub struct Uptime(u8);
|
||||
|
||||
impl Uptime {
|
||||
@@ -97,7 +104,7 @@ impl TryFrom<i64> for Uptime {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct MixnodeStatusReport {
|
||||
pub(crate) identity: String,
|
||||
pub(crate) owner: String,
|
||||
@@ -134,7 +141,7 @@ impl MixnodeStatusReport {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct GatewayStatusReport {
|
||||
pub(crate) identity: String,
|
||||
pub(crate) owner: String,
|
||||
@@ -171,7 +178,7 @@ impl GatewayStatusReport {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct MixnodeUptimeHistory {
|
||||
pub(crate) identity: String,
|
||||
pub(crate) owner: String,
|
||||
@@ -189,7 +196,7 @@ impl MixnodeUptimeHistory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct GatewayUptimeHistory {
|
||||
pub(crate) identity: String,
|
||||
pub(crate) owner: String,
|
||||
@@ -207,7 +214,7 @@ impl GatewayUptimeHistory {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, JsonSchema)]
|
||||
pub struct HistoricalUptime {
|
||||
// ISO 8601 date string
|
||||
// I think this is more than enough, we don't need the uber precision of timezone offsets, etc
|
||||
@@ -240,6 +247,43 @@ impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse {
|
||||
}
|
||||
}
|
||||
|
||||
impl JsonSchema for ErrorResponse {
|
||||
fn schema_name() -> String {
|
||||
"ErrorResponse".to_owned()
|
||||
}
|
||||
|
||||
fn json_schema(gen: &mut SchemaGenerator) -> Schema {
|
||||
let mut schema_object = SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
..SchemaObject::default()
|
||||
};
|
||||
|
||||
let object_validation = schema_object.object();
|
||||
object_validation
|
||||
.properties
|
||||
.insert("error_message".to_owned(), gen.subschema_for::<String>());
|
||||
object_validation
|
||||
.required
|
||||
.insert("error_message".to_owned());
|
||||
|
||||
// Status does not implement JsonSchema so we just explicitly specify the inner type.
|
||||
object_validation
|
||||
.properties
|
||||
.insert("status".to_owned(), gen.subschema_for::<u16>());
|
||||
object_validation.required.insert("status".to_owned());
|
||||
|
||||
Schema::Object(schema_object)
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenApiResponderInner for ErrorResponse {
|
||||
fn responses(_gen: &mut OpenApiGenerator) -> rocket_okapi::Result<Responses> {
|
||||
let mut responses = Responses::default();
|
||||
ensure_status_code_exists(&mut responses, 404);
|
||||
Ok(responses)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ValidatorApiStorageError {
|
||||
MixnodeReportNotFound(String),
|
||||
|
||||
@@ -11,6 +11,7 @@ use mixnet_contract_common::reward_params::{NodeRewardParams, RewardParams};
|
||||
use rocket::http::Status;
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use rocket_okapi::openapi;
|
||||
use validator_api_requests::models::{
|
||||
CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse,
|
||||
RewardEstimationResponse, StakeSaturationResponse,
|
||||
@@ -18,6 +19,7 @@ use validator_api_requests::models::{
|
||||
|
||||
use super::models::Uptime;
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/mixnode/<identity>/report")]
|
||||
pub(crate) async fn mixnode_report(
|
||||
storage: &State<ValidatorApiStorage>,
|
||||
@@ -30,6 +32,7 @@ pub(crate) async fn mixnode_report(
|
||||
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/gateway/<identity>/report")]
|
||||
pub(crate) async fn gateway_report(
|
||||
storage: &State<ValidatorApiStorage>,
|
||||
@@ -42,6 +45,7 @@ pub(crate) async fn gateway_report(
|
||||
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/mixnode/<identity>/history")]
|
||||
pub(crate) async fn mixnode_uptime_history(
|
||||
storage: &State<ValidatorApiStorage>,
|
||||
@@ -54,6 +58,7 @@ pub(crate) async fn mixnode_uptime_history(
|
||||
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/gateway/<identity>/history")]
|
||||
pub(crate) async fn gateway_uptime_history(
|
||||
storage: &State<ValidatorApiStorage>,
|
||||
@@ -66,6 +71,7 @@ pub(crate) async fn gateway_uptime_history(
|
||||
.map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/mixnode/<identity>/core-status-count?<since>")]
|
||||
pub(crate) async fn mixnode_core_status_count(
|
||||
storage: &State<ValidatorApiStorage>,
|
||||
@@ -83,6 +89,7 @@ pub(crate) async fn mixnode_core_status_count(
|
||||
})
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/gateway/<identity>/core-status-count?<since>")]
|
||||
pub(crate) async fn gateway_core_status_count(
|
||||
storage: &State<ValidatorApiStorage>,
|
||||
@@ -100,6 +107,7 @@ pub(crate) async fn gateway_core_status_count(
|
||||
})
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/mixnode/<identity>/status")]
|
||||
pub(crate) async fn get_mixnode_status(
|
||||
cache: &State<ValidatorCache>,
|
||||
@@ -110,6 +118,7 @@ pub(crate) async fn get_mixnode_status(
|
||||
})
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/mixnode/<identity>/reward-estimation")]
|
||||
pub(crate) async fn get_mixnode_reward_estimation(
|
||||
cache: &State<ValidatorCache>,
|
||||
@@ -165,6 +174,7 @@ pub(crate) async fn get_mixnode_reward_estimation(
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/mixnode/<identity>/stake-saturation")]
|
||||
pub(crate) async fn get_mixnode_stake_saturation(
|
||||
cache: &State<ValidatorCache>,
|
||||
@@ -193,6 +203,7 @@ pub(crate) async fn get_mixnode_stake_saturation(
|
||||
}
|
||||
}
|
||||
|
||||
#[openapi(tag = "mixnode")]
|
||||
#[get("/mixnode/<identity>/inclusion-probability")]
|
||||
pub(crate) async fn get_mixnode_inclusion_probability(
|
||||
cache: &State<ValidatorCache>,
|
||||
|
||||
@@ -6,9 +6,9 @@ edition = "2021"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
mixnet-contract-common = { path= ".../../../../common/cosmwasm-smart-contracts/mixnet-contract" }
|
||||
schemars = { version = "0.8", features = ["preserve_order"] }
|
||||
serde = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
ts-rs = "6.1.2"
|
||||
@@ -6,7 +6,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
test,
|
||||
@@ -26,7 +26,7 @@ impl MixnodeStatus {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
test,
|
||||
@@ -40,7 +40,7 @@ pub struct CoreNodeStatusResponse {
|
||||
pub count: i32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
test,
|
||||
@@ -53,7 +53,7 @@ pub struct MixnodeStatusResponse {
|
||||
pub status: MixnodeStatus,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct RewardEstimationResponse {
|
||||
pub estimated_total_node_reward: u64,
|
||||
pub estimated_operator_reward: u64,
|
||||
@@ -63,7 +63,7 @@ pub struct RewardEstimationResponse {
|
||||
pub as_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
test,
|
||||
@@ -118,7 +118,7 @@ impl fmt::Display for SelectionChance {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[cfg_attr(test, derive(ts_rs::TS))]
|
||||
#[cfg_attr(
|
||||
test,
|
||||
|
||||
Reference in New Issue
Block a user