Skip to content

Commit 78c28b0

Browse files
committed
add explorer crate
1 parent 0f9688a commit 78c28b0

13 files changed

+3768
-450
lines changed

Cargo.lock

+717-450
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ members = [
33
"jormungandr-lib",
44
"jormungandr",
55
"jcli",
6+
"explorer",
67
"modules/settings",
78
"modules/blockchain",
89
"testing/jormungandr-testing-utils",

explorer/Cargo.toml

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[package]
2+
authors = ["[email protected]"]
3+
description = "explorer service for jormungandr"
4+
documentation = "https://github.com/input-output-hk/jormungandr#USAGE.md"
5+
edition = "2018"
6+
homepage = "https://github.com/input-output-hk/jormungandr#README.md"
7+
license = "MIT OR Apache-2.0"
8+
name = "explorer"
9+
repository = "https://github.com/input-output-hk/jormungandr"
10+
version = "0.9.1"
11+
12+
[dependencies]
13+
futures = "0.3.5"
14+
futures-channel = "0.3.5"
15+
futures-util = "0.3.5"
16+
async-graphql = "2.9.15"
17+
async-graphql-warp = "2.9.15"
18+
serde = {version = "1.0.114", features = ["derive"]}
19+
serde_json = "1.0.56"
20+
serde_yaml = "0.8.13"
21+
structopt = "0.3.15"
22+
thiserror = "1.0.20"
23+
anyhow = "1.0.41"
24+
url = "2.1.1"
25+
warp = {version = "0.3.1", features = ["tls"]}
26+
tracing = "0.1"
27+
tracing-futures = "0.2"
28+
tracing-gelf = { version = "0.5", optional = true }
29+
tracing-journald = { version = "0.1.0", optional = true }
30+
tracing-subscriber = { version = "0.2", features = ["fmt", "json"] }
31+
tracing-appender = "0.1.2"
32+
tokio = { version = "^1.4", features = ["rt-multi-thread", "time", "sync", "rt", "signal", "test-util"] }
33+
tokio-stream = { version = "0.1.4", features = ["sync"] }
34+
tokio-util = { version = "0.6.0", features = ["time"] }
35+
tonic = "0.5.2"
36+
multiaddr = { package = "parity-multiaddr", version = "0.11" }
37+
rand = "0.8.3"
38+
rand_chacha = "0.3.1"
39+
base64 = "0.13.0"
40+
lazy_static = "1.4"
41+
sanakirja = "1.2.5"
42+
zerocopy = "0.5.0"
43+
byteorder = "1.4.3"
44+
hex = "0.4.3"
45+
46+
jormungandr-lib = {path = "../jormungandr-lib"}
47+
48+
chain-addr = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
49+
chain-core = {git = "https://github.com/input-output-hk/chain-libs", branch = "master"}
50+
chain-crypto = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
51+
chain-impl-mockchain = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
52+
chain-time = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
53+
chain-vote = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
54+
chain-ser = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
55+
chain-network = { git = "https://github.com/input-output-hk/chain-libs.git", branch = "chain-explorer" }
56+
chain-explorer = {git = "https://github.com/input-output-hk/chain-libs", branch = "chain-explorer"}
+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use super::{
2+
error::ApiError,
3+
scalars::{PayloadType, VotePlanId},
4+
BlockDate, Proposal,
5+
};
6+
use async_graphql::{FieldResult, Object, Union};
7+
use chain_explorer::{self, chain_storable, chain_storable::VotePlanMeta, schema::Txn};
8+
use std::sync::Arc;
9+
use tokio::sync::Mutex;
10+
11+
// interface for grouping certificates as a graphl union
12+
#[derive(Union)]
13+
pub enum Certificate {
14+
VotePlan(VotePlanCertificate),
15+
PublicVoteCast(PublicVoteCastCertificate),
16+
PrivateVoteCast(PrivateVoteCastCertificate),
17+
}
18+
19+
pub struct VotePlanCertificate {
20+
pub data: chain_storable::StorableHash,
21+
pub txn: Arc<Txn>,
22+
pub meta: Mutex<Option<VotePlanMeta>>,
23+
}
24+
25+
pub struct PublicVoteCastCertificate {
26+
pub data: chain_storable::PublicVoteCast,
27+
}
28+
29+
pub struct PrivateVoteCastCertificate {
30+
pub data: chain_storable::PrivateVoteCast,
31+
}
32+
33+
impl VotePlanCertificate {
34+
pub async fn get_meta(&self) -> FieldResult<VotePlanMeta> {
35+
let mut guard = self.meta.lock().await;
36+
37+
if let Some(meta) = &*guard {
38+
return Ok(meta.clone());
39+
}
40+
41+
let data = self.data.clone();
42+
43+
let txn = Arc::clone(&self.txn);
44+
let meta = tokio::task::spawn_blocking(move || {
45+
txn.get_vote_plan_meta(&data).map(|option| option.cloned())
46+
})
47+
.await
48+
.unwrap()?
49+
.unwrap();
50+
51+
*guard = Some(meta.clone());
52+
53+
Ok(meta)
54+
}
55+
}
56+
57+
#[Object]
58+
impl VotePlanCertificate {
59+
/// the vote start validity
60+
pub async fn vote_start(&self) -> FieldResult<BlockDate> {
61+
Ok(self.get_meta().await?.vote_start.into())
62+
}
63+
64+
/// the duration within which it is possible to vote for one of the proposals
65+
/// of this voting plan.
66+
pub async fn vote_end(&self) -> FieldResult<BlockDate> {
67+
Ok(self.get_meta().await?.vote_end.into())
68+
}
69+
70+
/// the committee duration is the time allocated to the committee to open
71+
/// the ballots and publish the results on chain
72+
pub async fn committee_end(&self) -> FieldResult<BlockDate> {
73+
Ok(self.get_meta().await?.committee_end.into())
74+
}
75+
76+
pub async fn payload_type(&self) -> FieldResult<PayloadType> {
77+
match self.get_meta().await?.payload_type {
78+
chain_explorer::chain_storable::PayloadType::Public => Ok(PayloadType::Public),
79+
chain_explorer::chain_storable::PayloadType::Private => Ok(PayloadType::Private),
80+
}
81+
}
82+
83+
/// the proposals to vote for
84+
pub async fn proposals(&self) -> FieldResult<Vec<Proposal>> {
85+
// TODO: add pagination
86+
Err(ApiError::Unimplemented.into())
87+
}
88+
}
89+
90+
#[Object]
91+
impl PublicVoteCastCertificate {
92+
pub async fn vote_plan(&self) -> VotePlanId {
93+
self.data.vote_plan_id.clone().into()
94+
}
95+
96+
pub async fn proposal_index(&self) -> u8 {
97+
self.data.proposal_index
98+
}
99+
100+
pub async fn choice(&self) -> u8 {
101+
self.data.choice
102+
}
103+
}
104+
105+
#[Object]
106+
impl PrivateVoteCastCertificate {
107+
pub async fn vote_plan(&self) -> VotePlanId {
108+
self.data.vote_plan_id.clone().into()
109+
}
110+
111+
pub async fn proposal_index(&self) -> u8 {
112+
self.data.proposal_index
113+
}
114+
}
+188
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
use async_graphql::{FieldResult, OutputType, SimpleObject};
2+
use std::convert::TryFrom;
3+
4+
#[derive(SimpleObject)]
5+
pub struct ConnectionFields<C: OutputType + Send + Sync> {
6+
pub total_count: C,
7+
}
8+
9+
pub struct ValidatedPaginationArguments<I> {
10+
pub first: Option<usize>,
11+
pub last: Option<usize>,
12+
pub before: Option<I>,
13+
pub after: Option<I>,
14+
}
15+
16+
pub struct PageMeta {
17+
pub has_next_page: bool,
18+
pub has_previous_page: bool,
19+
pub total_count: u64,
20+
}
21+
22+
fn compute_range_boundaries(
23+
total_elements: InclusivePaginationInterval<u64>,
24+
pagination_arguments: ValidatedPaginationArguments<u64>,
25+
) -> PaginationInterval<u64>
26+
where
27+
{
28+
use std::cmp::{max, min};
29+
30+
let InclusivePaginationInterval {
31+
upper_bound,
32+
lower_bound,
33+
} = total_elements;
34+
35+
// Compute the required range of blocks in two variables: [from, to]
36+
// Both ends are inclusive
37+
let mut from: u64 = match pagination_arguments.after {
38+
Some(cursor) => max(cursor + 1, lower_bound),
39+
// If `after` is not set, start from the beginning
40+
None => lower_bound,
41+
};
42+
43+
let mut to: u64 = match pagination_arguments.before {
44+
Some(cursor) => {
45+
if cursor == 0 {
46+
return PaginationInterval::Empty;
47+
}
48+
min(cursor - 1, upper_bound)
49+
}
50+
// If `before` is not set, start from the beginning
51+
None => upper_bound,
52+
};
53+
54+
// Move `to` enough values to make the result have `first` blocks
55+
if let Some(first) = pagination_arguments.first {
56+
to = min(
57+
from.checked_add(u64::try_from(first).unwrap())
58+
.and_then(|n| n.checked_sub(1))
59+
.unwrap_or(to),
60+
to,
61+
);
62+
}
63+
64+
// Move `from` enough values to make the result have `last` blocks
65+
if let Some(last) = pagination_arguments.last {
66+
from = max(
67+
to.checked_sub(u64::try_from(last).unwrap())
68+
.and_then(|n| n.checked_add(1))
69+
.unwrap_or(from),
70+
from,
71+
);
72+
}
73+
74+
PaginationInterval::Inclusive(InclusivePaginationInterval {
75+
lower_bound: from,
76+
upper_bound: to,
77+
})
78+
}
79+
80+
pub fn compute_interval<I>(
81+
bounds: PaginationInterval<I>,
82+
pagination_arguments: ValidatedPaginationArguments<I>,
83+
) -> FieldResult<(PaginationInterval<I>, PageMeta)>
84+
where
85+
I: TryFrom<u64> + Clone,
86+
u64: From<I>,
87+
{
88+
let pagination_arguments = pagination_arguments.cursors_into::<u64>();
89+
let bounds = bounds.bounds_into::<u64>();
90+
91+
let (page_interval, has_next_page, has_previous_page, total_count) = match bounds {
92+
PaginationInterval::Empty => (PaginationInterval::Empty, false, false, 0u64),
93+
PaginationInterval::Inclusive(total_elements) => {
94+
let InclusivePaginationInterval {
95+
upper_bound,
96+
lower_bound,
97+
} = total_elements;
98+
99+
let page = compute_range_boundaries(total_elements, pagination_arguments);
100+
101+
let (has_previous_page, has_next_page) = match &page {
102+
PaginationInterval::Empty => (false, false),
103+
PaginationInterval::Inclusive(page) => (
104+
page.lower_bound > lower_bound,
105+
page.upper_bound < upper_bound,
106+
),
107+
};
108+
109+
let total_count = upper_bound
110+
.checked_add(1)
111+
.unwrap()
112+
.checked_sub(lower_bound)
113+
.expect("upper_bound should be >= than lower_bound");
114+
(page, has_next_page, has_previous_page, total_count)
115+
}
116+
};
117+
118+
Ok(page_interval
119+
.bounds_try_into::<I>()
120+
.map(|interval| {
121+
(
122+
interval,
123+
PageMeta {
124+
has_next_page,
125+
has_previous_page,
126+
total_count,
127+
},
128+
)
129+
})
130+
.map_err(|_| "computed page interval is outside pagination boundaries")
131+
.unwrap())
132+
}
133+
134+
impl<I> ValidatedPaginationArguments<I> {
135+
fn cursors_into<T>(self) -> ValidatedPaginationArguments<T>
136+
where
137+
T: From<I>,
138+
{
139+
ValidatedPaginationArguments {
140+
after: self.after.map(T::from),
141+
before: self.before.map(T::from),
142+
first: self.first,
143+
last: self.last,
144+
}
145+
}
146+
}
147+
148+
pub enum PaginationInterval<I> {
149+
Empty,
150+
Inclusive(InclusivePaginationInterval<I>),
151+
}
152+
153+
pub struct InclusivePaginationInterval<I> {
154+
pub lower_bound: I,
155+
pub upper_bound: I,
156+
}
157+
158+
impl<I> PaginationInterval<I> {
159+
fn bounds_into<T>(self) -> PaginationInterval<T>
160+
where
161+
T: From<I>,
162+
{
163+
match self {
164+
Self::Empty => PaginationInterval::<T>::Empty,
165+
Self::Inclusive(interval) => {
166+
PaginationInterval::<T>::Inclusive(InclusivePaginationInterval::<T> {
167+
lower_bound: T::from(interval.lower_bound),
168+
upper_bound: T::from(interval.upper_bound),
169+
})
170+
}
171+
}
172+
}
173+
174+
fn bounds_try_into<T>(self) -> Result<PaginationInterval<T>, <T as TryFrom<I>>::Error>
175+
where
176+
T: TryFrom<I>,
177+
{
178+
match self {
179+
Self::Empty => Ok(PaginationInterval::<T>::Empty),
180+
Self::Inclusive(interval) => Ok(PaginationInterval::<T>::Inclusive(
181+
InclusivePaginationInterval::<T> {
182+
lower_bound: T::try_from(interval.lower_bound)?,
183+
upper_bound: T::try_from(interval.upper_bound)?,
184+
},
185+
)),
186+
}
187+
}
188+
}

explorer/src/api/graphql/error.rs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Debug)]
4+
pub enum ApiError {
5+
#[error("internal error (this shouldn't happen) {0}")]
6+
InternalError(String),
7+
#[error("internal error (this shouldn't happen)")]
8+
InternalDbError,
9+
#[error("resource not found {0}")]
10+
NotFound(String),
11+
#[error("feature not implemented yet")]
12+
Unimplemented,
13+
#[error("invalid argument {0}")]
14+
ArgumentError(String),
15+
#[error("invalud pagination cursor {0}")]
16+
InvalidCursor(String),
17+
#[error("invalid address {0}")]
18+
InvalidAddress(String),
19+
}

0 commit comments

Comments
 (0)