Skip to content

Commit

Permalink
add explorer crate
Browse files Browse the repository at this point in the history
  • Loading branch information
ecioppettini committed Oct 20, 2021
1 parent 0f9688a commit 78c28b0
Show file tree
Hide file tree
Showing 13 changed files with 3,768 additions and 450 deletions.
1,167 changes: 717 additions & 450 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ members = [
"jormungandr-lib",
"jormungandr",
"jcli",
"explorer",
"modules/settings",
"modules/blockchain",
"testing/jormungandr-testing-utils",
Expand Down
56 changes: 56 additions & 0 deletions explorer/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[package]
authors = ["[email protected]"]
description = "explorer service for jormungandr"
documentation = "https://github.com/input-output-hk/jormungandr#USAGE.md"
edition = "2018"
homepage = "https://github.com/input-output-hk/jormungandr#README.md"
license = "MIT OR Apache-2.0"
name = "explorer"
repository = "https://github.com/input-output-hk/jormungandr"
version = "0.9.1"

[dependencies]
futures = "0.3.5"
futures-channel = "0.3.5"
futures-util = "0.3.5"
async-graphql = "2.9.15"
async-graphql-warp = "2.9.15"
serde = {version = "1.0.114", features = ["derive"]}
serde_json = "1.0.56"
serde_yaml = "0.8.13"
structopt = "0.3.15"
thiserror = "1.0.20"
anyhow = "1.0.41"
url = "2.1.1"
warp = {version = "0.3.1", features = ["tls"]}
tracing = "0.1"
tracing-futures = "0.2"
tracing-gelf = { version = "0.5", optional = true }
tracing-journald = { version = "0.1.0", optional = true }
tracing-subscriber = { version = "0.2", features = ["fmt", "json"] }
tracing-appender = "0.1.2"
tokio = { version = "^1.4", features = ["rt-multi-thread", "time", "sync", "rt", "signal", "test-util"] }
tokio-stream = { version = "0.1.4", features = ["sync"] }
tokio-util = { version = "0.6.0", features = ["time"] }
tonic = "0.5.2"
multiaddr = { package = "parity-multiaddr", version = "0.11" }
rand = "0.8.3"
rand_chacha = "0.3.1"
base64 = "0.13.0"
lazy_static = "1.4"
sanakirja = "1.2.5"
zerocopy = "0.5.0"
byteorder = "1.4.3"
hex = "0.4.3"

jormungandr-lib = {path = "../jormungandr-lib"}

chain-addr = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
chain-core = {git = "https://github.com/input-output-hk/chain-libs", branch = "master"}
chain-crypto = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
chain-impl-mockchain = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
chain-time = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
chain-vote = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
chain-ser = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
chain-network = { git = "https://github.com/input-output-hk/chain-libs.git", branch = "chain-explorer" }
chain-explorer = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
114 changes: 114 additions & 0 deletions explorer/src/api/graphql/certificates.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use super::{
error::ApiError,
scalars::{PayloadType, VotePlanId},
BlockDate, Proposal,
};
use async_graphql::{FieldResult, Object, Union};
use chain_explorer::{self, chain_storable, chain_storable::VotePlanMeta, schema::Txn};
use std::sync::Arc;
use tokio::sync::Mutex;

// interface for grouping certificates as a graphl union
#[derive(Union)]
pub enum Certificate {
VotePlan(VotePlanCertificate),
PublicVoteCast(PublicVoteCastCertificate),
PrivateVoteCast(PrivateVoteCastCertificate),
}

pub struct VotePlanCertificate {
pub data: chain_storable::StorableHash,
pub txn: Arc<Txn>,
pub meta: Mutex<Option<VotePlanMeta>>,
}

pub struct PublicVoteCastCertificate {
pub data: chain_storable::PublicVoteCast,
}

pub struct PrivateVoteCastCertificate {
pub data: chain_storable::PrivateVoteCast,
}

impl VotePlanCertificate {
pub async fn get_meta(&self) -> FieldResult<VotePlanMeta> {
let mut guard = self.meta.lock().await;

if let Some(meta) = &*guard {
return Ok(meta.clone());
}

let data = self.data.clone();

let txn = Arc::clone(&self.txn);
let meta = tokio::task::spawn_blocking(move || {
txn.get_vote_plan_meta(&data).map(|option| option.cloned())
})
.await
.unwrap()?
.unwrap();

*guard = Some(meta.clone());

Ok(meta)
}
}

#[Object]
impl VotePlanCertificate {
/// the vote start validity
pub async fn vote_start(&self) -> FieldResult<BlockDate> {
Ok(self.get_meta().await?.vote_start.into())
}

/// the duration within which it is possible to vote for one of the proposals
/// of this voting plan.
pub async fn vote_end(&self) -> FieldResult<BlockDate> {
Ok(self.get_meta().await?.vote_end.into())
}

/// the committee duration is the time allocated to the committee to open
/// the ballots and publish the results on chain
pub async fn committee_end(&self) -> FieldResult<BlockDate> {
Ok(self.get_meta().await?.committee_end.into())
}

pub async fn payload_type(&self) -> FieldResult<PayloadType> {
match self.get_meta().await?.payload_type {
chain_explorer::chain_storable::PayloadType::Public => Ok(PayloadType::Public),
chain_explorer::chain_storable::PayloadType::Private => Ok(PayloadType::Private),
}
}

/// the proposals to vote for
pub async fn proposals(&self) -> FieldResult<Vec<Proposal>> {
// TODO: add pagination
Err(ApiError::Unimplemented.into())
}
}

#[Object]
impl PublicVoteCastCertificate {
pub async fn vote_plan(&self) -> VotePlanId {
self.data.vote_plan_id.clone().into()
}

pub async fn proposal_index(&self) -> u8 {
self.data.proposal_index
}

pub async fn choice(&self) -> u8 {
self.data.choice
}
}

#[Object]
impl PrivateVoteCastCertificate {
pub async fn vote_plan(&self) -> VotePlanId {
self.data.vote_plan_id.clone().into()
}

pub async fn proposal_index(&self) -> u8 {
self.data.proposal_index
}
}
188 changes: 188 additions & 0 deletions explorer/src/api/graphql/connections.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
use async_graphql::{FieldResult, OutputType, SimpleObject};
use std::convert::TryFrom;

#[derive(SimpleObject)]
pub struct ConnectionFields<C: OutputType + Send + Sync> {
pub total_count: C,
}

pub struct ValidatedPaginationArguments<I> {
pub first: Option<usize>,
pub last: Option<usize>,
pub before: Option<I>,
pub after: Option<I>,
}

pub struct PageMeta {
pub has_next_page: bool,
pub has_previous_page: bool,
pub total_count: u64,
}

fn compute_range_boundaries(
total_elements: InclusivePaginationInterval<u64>,
pagination_arguments: ValidatedPaginationArguments<u64>,
) -> PaginationInterval<u64>
where
{
use std::cmp::{max, min};

let InclusivePaginationInterval {
upper_bound,
lower_bound,
} = total_elements;

// Compute the required range of blocks in two variables: [from, to]
// Both ends are inclusive
let mut from: u64 = match pagination_arguments.after {
Some(cursor) => max(cursor + 1, lower_bound),
// If `after` is not set, start from the beginning
None => lower_bound,
};

let mut to: u64 = match pagination_arguments.before {
Some(cursor) => {
if cursor == 0 {
return PaginationInterval::Empty;
}
min(cursor - 1, upper_bound)
}
// If `before` is not set, start from the beginning
None => upper_bound,
};

// Move `to` enough values to make the result have `first` blocks
if let Some(first) = pagination_arguments.first {
to = min(
from.checked_add(u64::try_from(first).unwrap())
.and_then(|n| n.checked_sub(1))
.unwrap_or(to),
to,
);
}

// Move `from` enough values to make the result have `last` blocks
if let Some(last) = pagination_arguments.last {
from = max(
to.checked_sub(u64::try_from(last).unwrap())
.and_then(|n| n.checked_add(1))
.unwrap_or(from),
from,
);
}

PaginationInterval::Inclusive(InclusivePaginationInterval {
lower_bound: from,
upper_bound: to,
})
}

pub fn compute_interval<I>(
bounds: PaginationInterval<I>,
pagination_arguments: ValidatedPaginationArguments<I>,
) -> FieldResult<(PaginationInterval<I>, PageMeta)>
where
I: TryFrom<u64> + Clone,
u64: From<I>,
{
let pagination_arguments = pagination_arguments.cursors_into::<u64>();
let bounds = bounds.bounds_into::<u64>();

let (page_interval, has_next_page, has_previous_page, total_count) = match bounds {
PaginationInterval::Empty => (PaginationInterval::Empty, false, false, 0u64),
PaginationInterval::Inclusive(total_elements) => {
let InclusivePaginationInterval {
upper_bound,
lower_bound,
} = total_elements;

let page = compute_range_boundaries(total_elements, pagination_arguments);

let (has_previous_page, has_next_page) = match &page {
PaginationInterval::Empty => (false, false),
PaginationInterval::Inclusive(page) => (
page.lower_bound > lower_bound,
page.upper_bound < upper_bound,
),
};

let total_count = upper_bound
.checked_add(1)
.unwrap()
.checked_sub(lower_bound)
.expect("upper_bound should be >= than lower_bound");
(page, has_next_page, has_previous_page, total_count)
}
};

Ok(page_interval
.bounds_try_into::<I>()
.map(|interval| {
(
interval,
PageMeta {
has_next_page,
has_previous_page,
total_count,
},
)
})
.map_err(|_| "computed page interval is outside pagination boundaries")
.unwrap())
}

impl<I> ValidatedPaginationArguments<I> {
fn cursors_into<T>(self) -> ValidatedPaginationArguments<T>
where
T: From<I>,
{
ValidatedPaginationArguments {
after: self.after.map(T::from),
before: self.before.map(T::from),
first: self.first,
last: self.last,
}
}
}

pub enum PaginationInterval<I> {
Empty,
Inclusive(InclusivePaginationInterval<I>),
}

pub struct InclusivePaginationInterval<I> {
pub lower_bound: I,
pub upper_bound: I,
}

impl<I> PaginationInterval<I> {
fn bounds_into<T>(self) -> PaginationInterval<T>
where
T: From<I>,
{
match self {
Self::Empty => PaginationInterval::<T>::Empty,
Self::Inclusive(interval) => {
PaginationInterval::<T>::Inclusive(InclusivePaginationInterval::<T> {
lower_bound: T::from(interval.lower_bound),
upper_bound: T::from(interval.upper_bound),
})
}
}
}

fn bounds_try_into<T>(self) -> Result<PaginationInterval<T>, <T as TryFrom<I>>::Error>
where
T: TryFrom<I>,
{
match self {
Self::Empty => Ok(PaginationInterval::<T>::Empty),
Self::Inclusive(interval) => Ok(PaginationInterval::<T>::Inclusive(
InclusivePaginationInterval::<T> {
lower_bound: T::try_from(interval.lower_bound)?,
upper_bound: T::try_from(interval.upper_bound)?,
},
)),
}
}
}
19 changes: 19 additions & 0 deletions explorer/src/api/graphql/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use thiserror::Error;

#[derive(Error, Debug)]
pub enum ApiError {
#[error("internal error (this shouldn't happen) {0}")]
InternalError(String),
#[error("internal error (this shouldn't happen)")]
InternalDbError,
#[error("resource not found {0}")]
NotFound(String),
#[error("feature not implemented yet")]
Unimplemented,
#[error("invalid argument {0}")]
ArgumentError(String),
#[error("invalud pagination cursor {0}")]
InvalidCursor(String),
#[error("invalid address {0}")]
InvalidAddress(String),
}
Loading

0 comments on commit 78c28b0

Please sign in to comment.