-
Notifications
You must be signed in to change notification settings - Fork 132
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0f9688a
commit 78c28b0
Showing
13 changed files
with
3,768 additions
and
450 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)?, | ||
}, | ||
)), | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
} |
Oops, something went wrong.