diff --git a/.cargo/config.toml b/.cargo/config.toml index 7c6dfe8e9..31642d856 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,16 +1,16 @@ [alias] -ci-build-all = "build --all-targets --all-features" -ci-build-inx = "build --all-targets --no-default-features --features inx,stardust" -ci-build-api = "build --all-targets --no-default-features --features api-core,api-history,analytics,stardust" +ci-check-all = "check --all-targets --all-features" +ci-check-inx = "check --all-targets --no-default-features --features inx,stardust" +ci-check-api = "check --all-targets --no-default-features --features api,stardust" ci-clippy-all = "clippy --all-targets --all-features -- -D warnings" ci-clippy-inx = "clippy --all-targets --no-default-features --features inx,stardust -- -D warnings" -ci-clippy-api = "clippy --all-targets --no-default-features --features api-core,api-history,analytics,stardust -- -D warnings" +ci-clippy-api = "clippy --all-targets --no-default-features --features api,stardust -- -D warnings" -ci-doctest = "test --doc --all-features --release" +ci-doctest = "test --doc --all-features" ci-doc = "doc --all-features --no-deps --document-private-items" ci-fmt = "fmt --all -- --check" -ci-test = "test --all-targets --all-features --release" +ci-test = "test --all-targets --all-features" ci-toml = "sort --grouped --check" ci-udeps = "udeps --all-targets --all-features --backend=depinfo" diff --git a/.github/workflows/_build.yml b/.github/workflows/_check.yml similarity index 85% rename from .github/workflows/_build.yml rename to .github/workflows/_check.yml index a258fb6ac..d9da71c49 100644 --- a/.github/workflows/_build.yml +++ b/.github/workflows/_check.yml @@ -1,4 +1,4 @@ -name: Build and Test +name: Check and Test on: workflow_call: @@ -11,7 +11,7 @@ on: type: string jobs: - build-and-test: + check-and-test: name: '${{ inputs.os }}, ${{ inputs.rust }}' runs-on: ${{ inputs.os }} # Unfortunately, we can't do this right now because `indexmap` does not seem to follow semver. @@ -42,22 +42,22 @@ jobs: - uses: Swatinem/rust-cache@v1 - - name: Build with all features + - name: Check (all features) uses: actions-rs/cargo@v1 with: - command: ci-build-all + command: ci-check-all - - name: Build with INX only + - name: Check (INX only) if: contains(inputs.os, 'ubuntu') uses: actions-rs/cargo@v1 with: - command: ci-build-inx + command: ci-check-inx - - name: Build with API only + - name: Check (API only) if: contains(inputs.os, 'ubuntu') uses: actions-rs/cargo@v1 with: - command: ci-build-api + command: ci-check-api - name: Test uses: actions-rs/cargo@v1 diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index deee912fa..b3cd86076 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -6,29 +6,29 @@ on: jobs: - build-and-test-1: - name: "build and test" - uses: ./.github/workflows/_build.yml + check-and-test-1: + name: "check and test" + uses: ./.github/workflows/_check.yml with: { os: windows-latest, rust: stable } - build-and-test-2: - name: "build and test" - uses: ./.github/workflows/_build.yml + check-and-test-2: + name: "check and test" + uses: ./.github/workflows/_check.yml with: { os: macos-latest, rust: stable } - build-and-test-3: - name: "build and test" - uses: ./.github/workflows/_build.yml + check-and-test-3: + name: "check and test" + uses: ./.github/workflows/_check.yml with: { os: ubuntu-latest, rust: beta } - build-and-test-4: - name: "build and test" - uses: ./.github/workflows/_build.yml + check-and-test-4: + name: "check and test" + uses: ./.github/workflows/_check.yml with: { os: windows-latest, rust: beta } - build-and-test-5: - name: "build and test" - uses: ./.github/workflows/_build.yml + check-and-test-5: + name: "check and test" + uses: ./.github/workflows/_check.yml with: { os: macos-latest, rust: beta } docker: @@ -58,4 +58,23 @@ jobs: with: command: ci-udeps + check-all-features: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - uses: actions-rs/cargo@v1 + with: + command: install + args: --force cargo-all-features + + - uses: actions-rs/cargo@v1 + with: + command: check-all-features diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 382e2cdec..1da2e2204 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,9 +15,9 @@ concurrency: cancel-in-progress: true jobs: - build-and-test: - name: "build and test" - uses: ./.github/workflows/_build.yml + check-and-test: + name: "check and test" + uses: ./.github/workflows/_check.yml with: { os: ubuntu-latest, rust: stable } format: diff --git a/Cargo.toml b/Cargo.toml index 6578cfbb0..11cc31f4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ uuid = { version = "1.1", default-features = false, features = ["v4"] } auth-helper = { version = "0.3", default-features = false, optional = true } axum = { version = "0.5", default-features = false, features = ["http1", "json", "query", "original-uri", "headers"], optional = true } ed25519 = { version = "1.5", default-features = false, features = ["alloc", "pkcs8", "pem"], optional = true } -ed25519-dalek = { version = "1.0", default-features = false, optional = true } +ed25519-dalek = { version = "1.0", default-features = false, features = ["u64_backend"], optional = true } hex = { version = "0.4", default-features = false, optional = true } hyper = { version = "0.14", default-features = false, features = ["server", "tcp", "stream"], optional = true } lazy_static = { version = "1.4", default-features = false, optional = true } @@ -82,14 +82,11 @@ packable = { version = "0.4", default-features = false } [features] default = [ - "analytics", - "api-history", - "api-core", + "api", "inx", - "stardust", "metrics", + "stardust", ] -analytics = [] api = [ "dep:auth-helper", "dep:axum", @@ -106,12 +103,7 @@ api = [ "dep:tower", "dep:tower-http", "dep:zeroize", -] -api-history = [ - "api", -] -api-core = [ - "api", + "stardust", ] console = [ "dep:console-subscriber", diff --git a/bin/inx-chronicle/src/api/extractors.rs b/bin/inx-chronicle/src/api/extractors.rs index b9f4e2c02..521e4642e 100644 --- a/bin/inx-chronicle/src/api/extractors.rs +++ b/bin/inx-chronicle/src/api/extractors.rs @@ -3,7 +3,6 @@ use async_trait::async_trait; use axum::extract::{FromRequest, Query}; -use chronicle::types::stardust::milestone::MilestoneTimestamp; use serde::Deserialize; use time::{Duration, OffsetDateTime}; @@ -44,43 +43,53 @@ pub struct TimeRangeQuery { end_timestamp: Option, } -#[derive(Copy, Clone)] -pub struct TimeRange { - pub start_timestamp: MilestoneTimestamp, - pub end_timestamp: MilestoneTimestamp, -} +#[cfg(feature = "stardust")] +mod stardust { + use chronicle::types::stardust::milestone::MilestoneTimestamp; -fn days_ago_utc(days: i64) -> u32 { - let then = OffsetDateTime::now_utc() - Duration::days(days); - then.unix_timestamp() as u32 -} + use super::*; -fn now_utc() -> u32 { - OffsetDateTime::now_utc().unix_timestamp() as u32 -} + #[derive(Copy, Clone)] + pub struct TimeRange { + pub start_timestamp: MilestoneTimestamp, + pub end_timestamp: MilestoneTimestamp, + } -#[async_trait] -impl FromRequest for TimeRange { - type Rejection = ApiError; + fn days_ago_utc(days: i64) -> u32 { + let then = OffsetDateTime::now_utc() - Duration::days(days); + then.unix_timestamp() as u32 + } - async fn from_request(req: &mut axum::extract::RequestParts) -> Result { - let Query(TimeRangeQuery { - start_timestamp, - end_timestamp, - }) = Query::::from_request(req) - .await - .map_err(ApiError::QueryError)?; - let time_range = TimeRange { - start_timestamp: start_timestamp.unwrap_or_else(|| days_ago_utc(30)).into(), - end_timestamp: end_timestamp.unwrap_or_else(now_utc).into(), - }; - if time_range.end_timestamp < time_range.start_timestamp { - return Err(ApiError::BadTimeRange); + fn now_utc() -> u32 { + OffsetDateTime::now_utc().unix_timestamp() as u32 + } + + #[async_trait] + impl FromRequest for TimeRange { + type Rejection = ApiError; + + async fn from_request(req: &mut axum::extract::RequestParts) -> Result { + let Query(TimeRangeQuery { + start_timestamp, + end_timestamp, + }) = Query::::from_request(req) + .await + .map_err(ApiError::QueryError)?; + let time_range = TimeRange { + start_timestamp: start_timestamp.unwrap_or_else(|| days_ago_utc(30)).into(), + end_timestamp: end_timestamp.unwrap_or_else(now_utc).into(), + }; + if time_range.end_timestamp < time_range.start_timestamp { + return Err(ApiError::BadTimeRange); + } + Ok(time_range) } - Ok(time_range) } } +#[cfg(feature = "stardust")] +pub use stardust::*; + #[derive(Copy, Clone, Deserialize)] #[serde(default)] pub struct Included { diff --git a/bin/inx-chronicle/src/api/responses.rs b/bin/inx-chronicle/src/api/responses.rs index e530df836..0e70b3518 100644 --- a/bin/inx-chronicle/src/api/responses.rs +++ b/bin/inx-chronicle/src/api/responses.rs @@ -1,10 +1,6 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -use chronicle::{ - db::collections::SyncData, - types::{ledger::LedgerInclusionState, tangle::MilestoneIndex}, -}; use serde::{Deserialize, Serialize}; macro_rules! impl_success_response { @@ -33,31 +29,6 @@ pub struct InfoResponse { impl_success_response!(InfoResponse); -/// An aggregation type that represents the ranges of completed milestones and gaps. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct SyncDataDto(pub SyncData); - -impl_success_response!(SyncDataDto); - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Record { - pub id: String, - pub inclusion_state: Option, - pub milestone_index: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct Transfer { - pub transaction_id: String, - pub output_index: u16, - pub is_spending: bool, - pub inclusion_state: Option, - pub block_id: String, - pub amount: u64, -} - #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct MaybeSpentOutput { @@ -71,3 +42,40 @@ pub struct Unlock { pub block_id: String, pub block: Value, } + +/// An aggregation type that represents the ranges of completed milestones and gaps. +#[cfg(feature = "stardust")] +mod stardust { + use chronicle::{ + db::collections::SyncData, + types::{ledger::LedgerInclusionState, tangle::MilestoneIndex}, + }; + use serde::{Deserialize, Serialize}; + + #[derive(Debug, Clone, Default, Serialize, Deserialize)] + pub struct SyncDataDto(pub SyncData); + + impl_success_response!(SyncDataDto); + + #[derive(Clone, Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Record { + pub id: String, + pub inclusion_state: Option, + pub milestone_index: Option, + } + + #[derive(Clone, Debug, Serialize, Deserialize)] + #[serde(rename_all = "camelCase")] + pub struct Transfer { + pub transaction_id: String, + pub output_index: u16, + pub is_spending: bool, + pub inclusion_state: Option, + pub block_id: String, + pub amount: u64, + } +} + +#[cfg(feature = "stardust")] +pub use stardust::*; diff --git a/bin/inx-chronicle/src/api/routes.rs b/bin/inx-chronicle/src/api/routes.rs index b78b2355b..a7ec2c8ee 100644 --- a/bin/inx-chronicle/src/api/routes.rs +++ b/bin/inx-chronicle/src/api/routes.rs @@ -13,7 +13,7 @@ use hyper::StatusCode; use serde::Deserialize; use time::{Duration, OffsetDateTime}; -use super::{auth::Auth, config::ApiData, error::ApiError, responses::*, ApiResult}; +use super::{auth::Auth, config::ApiData, error::ApiError, responses::*}; // Similar to Hornet, we enforce that the latest known milestone is newer than 5 minutes. This should give Chronicle // sufficient time to catch up with the node that it is connected too. The current milestone interval is 5 seconds. @@ -61,47 +61,54 @@ async fn login( } } -async fn is_healthy(database: Extension) -> bool { - let end = match database.get_latest_milestone().await { - Ok(Some(last)) => last, - _ => return false, - }; - - // Panic: The milestone_timestamp is guaranteeed to be valid. - let latest_ms_time = OffsetDateTime::from_unix_timestamp(end.milestone_timestamp.0 as i64).unwrap(); - - if OffsetDateTime::now_utc() > latest_ms_time + STALE_MILESTONE_DURATION { - return false; +async fn is_healthy(database: &MongoDb) -> Result { + #[cfg(feature = "stardust")] + { + let end = match database.get_latest_milestone().await? { + Some(last) => last, + None => return Ok(false), + }; + + // Panic: The milestone_timestamp is guaranteeed to be valid. + let latest_ms_time = OffsetDateTime::from_unix_timestamp(end.milestone_timestamp.0 as i64).unwrap(); + + if OffsetDateTime::now_utc() > latest_ms_time + STALE_MILESTONE_DURATION { + return Ok(false); + } + + // Check if there are no gaps in the sync status. + if !database.get_gaps().await?.is_empty() { + return Ok(false); + }; } - // Check if there are no gaps in the sync status. - match database.get_gaps().await { - Ok(gaps) => gaps.is_empty(), - _ => false, - } + Ok(true) } pub async fn info(database: Extension) -> InfoResponse { InfoResponse { name: "Chronicle".into(), version: std::env!("CARGO_PKG_VERSION").to_string(), - is_healthy: is_healthy(database).await, + is_healthy: is_healthy(&database).await.unwrap_or_else(|e| { + log::error!("An error occured during health check: {e}"); + false + }), } } pub async fn health(database: Extension) -> StatusCode { - if is_healthy(database).await { + let handle_error = |e| { + log::error!("An error occured during health check: {e}"); + false + }; + + if is_healthy(&database).await.unwrap_or_else(handle_error) { StatusCode::OK } else { StatusCode::SERVICE_UNAVAILABLE } } -#[cfg(feature = "api-history")] -pub async fn sync(database: Extension) -> ApiResult { - Ok(SyncDataDto(database.get_sync_data(0.into()..=u32::MAX.into()).await?)) -} - pub async fn not_found() -> ApiError { ApiError::NotFound } diff --git a/bin/inx-chronicle/src/api/stardust/history/routes.rs b/bin/inx-chronicle/src/api/stardust/history/routes.rs index da8fbae9a..984d1fd61 100644 --- a/bin/inx-chronicle/src/api/stardust/history/routes.rs +++ b/bin/inx-chronicle/src/api/stardust/history/routes.rs @@ -16,7 +16,7 @@ use super::{ TransactionsPerAddressResponse, TransactionsPerMilestoneResponse, TransferByAddress, TransferByMilestone, }, }; -use crate::api::{routes::sync, ApiError, ApiResult}; +use crate::api::{responses::SyncDataDto, ApiError, ApiResult}; pub fn routes() -> Router { Router::new().route("/gaps", get(sync)).nest( @@ -27,6 +27,10 @@ pub fn routes() -> Router { ) } +async fn sync(database: Extension) -> ApiResult { + Ok(SyncDataDto(database.get_sync_data(0.into()..=u32::MAX.into()).await?)) +} + async fn transactions_by_address_history( database: Extension, Path(address): Path, diff --git a/bin/inx-chronicle/src/api/stardust/mod.rs b/bin/inx-chronicle/src/api/stardust/mod.rs index 6e4852593..31b737de5 100644 --- a/bin/inx-chronicle/src/api/stardust/mod.rs +++ b/bin/inx-chronicle/src/api/stardust/mod.rs @@ -1,33 +1,15 @@ // Copyright 2022 IOTA Stiftung // SPDX-License-Identifier: Apache-2.0 -#[cfg(feature = "analytics")] pub mod analytics; -#[cfg(feature = "api-core")] pub mod core; -#[cfg(feature = "api-history")] pub mod history; use axum::Router; pub fn routes() -> Router { - #[allow(unused_mut)] - let mut router = Router::new(); - - #[cfg(feature = "analytics")] - { - router = router.nest("/analytics/v2", analytics::routes()); - } - - #[cfg(feature = "api-history")] - { - router = router.nest("/history/v2", history::routes()); - } - - #[cfg(feature = "api-core")] - { - router = router.nest("/core/v2", core::routes()); - } - - router + Router::new() + .nest("/analytics/v2", analytics::routes()) + .nest("/history/v2", history::routes()) + .nest("/core/v2", core::routes()) } diff --git a/bin/inx-chronicle/src/launcher.rs b/bin/inx-chronicle/src/launcher.rs index e5fa37862..0bf0bc083 100644 --- a/bin/inx-chronicle/src/launcher.rs +++ b/bin/inx-chronicle/src/launcher.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use async_trait::async_trait; +#[cfg(any(feature = "api", feature = "inx"))] +use chronicle::runtime::{ActorError, HandleEvent, Report}; use chronicle::{ db::MongoDb, - runtime::{Actor, ActorContext, ActorError, ErrorLevel, HandleEvent, Report, RuntimeError}, + runtime::{Actor, ActorContext, ErrorLevel, RuntimeError}, }; use clap::Parser; use thiserror::Error; @@ -44,6 +46,7 @@ impl Actor for Launcher { type State = ChronicleConfig; type Error = LauncherError; + #[allow(unused_variables)] async fn init(&mut self, cx: &mut ActorContext) -> Result { // TODO: move parsing command-line args to `main.rs` (only async closures make this low effort though) let cl_args = ClArgs::parse(); @@ -62,10 +65,13 @@ impl Actor for Launcher { let db = MongoDb::connect(&config.mongodb).await?; - db.create_output_indexes().await?; - db.create_block_indexes().await?; - db.create_ledger_update_indexes().await?; - db.create_milestone_indexes().await?; + #[cfg(feature = "stardust")] + { + db.create_output_indexes().await?; + db.create_block_indexes().await?; + db.create_ledger_update_indexes().await?; + db.create_milestone_indexes().await?; + } #[cfg(all(feature = "inx", feature = "stardust"))] if config.inx.enabled { diff --git a/src/db/collections/block.rs b/src/db/collections/block.rs index 0fcedb904..8302ae6ab 100644 --- a/src/db/collections/block.rs +++ b/src/db/collections/block.rs @@ -255,7 +255,6 @@ impl MongoDb { } } -#[cfg(feature = "analytics")] mod analytics { use super::*; use crate::types::tangle::MilestoneIndex; diff --git a/src/db/collections/ledger_update.rs b/src/db/collections/ledger_update.rs index 3e156d722..e34654ea4 100644 --- a/src/db/collections/ledger_update.rs +++ b/src/db/collections/ledger_update.rs @@ -264,7 +264,6 @@ impl MongoDb { } } -#[cfg(feature = "analytics")] mod analytics { use futures::TryStreamExt; use mongodb::bson; diff --git a/src/db/mod.rs b/src/db/mod.rs index cabd19cde..8455649e8 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 /// Module containing the collections in the database. +#[cfg(feature = "stardust")] pub mod collections; mod mongodb; diff --git a/src/types/mod.rs b/src/types/mod.rs index d1ed7357b..b8cb70255 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -4,11 +4,13 @@ #![allow(missing_docs)] // TODO Remove this once everything has settled. /// Module containing the ledger data models. +#[cfg(feature = "stardust")] pub mod ledger; /// Module containing Stardust data models. #[cfg(feature = "stardust")] pub mod stardust; /// Module containing the tangle models. +#[cfg(feature = "stardust")] pub mod tangle; /// Module contain utility functions. pub mod util;