#[cfg(feature = "archive")]
mod archive;
mod block;
mod data;
mod tx;
use block::*;
use data::*;
use tx::*;
use async_graphql::{Context, FieldError, FieldResult, Object};
use dusk_core::abi::ContractId;
use dusk_core::transfer::TRANSFER_CONTRACT;
use node::database::rocksdb::Backend;
use node::database::{DB, Ledger};
use node_data::ledger::Label;
#[cfg(feature = "archive")]
use {
archive::data::*, archive::events::*, archive::finalized_block::*,
archive::moonlight::*, node::archive::Archive,
};
use std::sync::Arc;
use tokio::sync::RwLock;
#[cfg(feature = "archive")]
pub type DBContext = (Arc<RwLock<Backend>>, Archive);
#[cfg(not(feature = "archive"))]
pub type DBContext = (Arc<RwLock<Backend>>, ());
pub type OptResult<T> = FieldResult<Option<T>>;
const MAX_GRAPHQL_BLOCKS_PER_QUERY: u64 = 200;
const MAX_GRAPHQL_TXS_PER_QUERY: u64 = 200;
pub struct Query;
#[Object]
impl Query {
async fn block(
&self,
ctx: &Context<'_>,
height: Option<f64>,
hash: Option<String>,
) -> OptResult<data::Block> {
let block = match (height, hash) {
(Some(height), None) => block_by_height(ctx, height).await,
(None, Some(hash)) => block_by_hash(ctx, hash).await,
_ => Err(FieldError::new("Specify heigth or hash")),
};
Ok(block?)
}
async fn tx(
&self,
ctx: &Context<'_>,
hash: String,
) -> OptResult<SpentTransaction> {
tx_by_hash(ctx, hash).await
}
async fn transactions(
&self,
ctx: &Context<'_>,
last: u64,
) -> FieldResult<Vec<SpentTransaction>> {
if last > MAX_GRAPHQL_TXS_PER_QUERY {
return Err(FieldError::new(format!(
"last too large (max {MAX_GRAPHQL_TXS_PER_QUERY})"
)));
}
last_transactions(ctx, last as usize).await
}
async fn block_txs(
&self,
ctx: &Context<'_>,
last: Option<u64>,
range: Option<[u64; 2]>,
contract: Option<String>,
) -> FieldResult<Vec<SpentTransaction>> {
let blocks = self.blocks(ctx, last, range).await?;
let contract = match contract {
Some(contract) => {
let mut decoded = [0u8; 32];
decoded.copy_from_slice(&hex::decode(contract)?[..]);
Some(ContractId::from(decoded))
}
_ => None,
};
let mut txs = vec![];
for b in blocks.iter() {
let mut block_txs = b.transactions(ctx).await?;
match contract.as_ref() {
None => txs.append(&mut block_txs),
Some(contract) => {
let mut txs_to_add = block_txs
.into_iter()
.filter(|t| {
let tx_contract =
t.0.inner
.inner
.call()
.map(|c| c.contract)
.unwrap_or(TRANSFER_CONTRACT);
tx_contract == *contract
})
.collect();
txs.append(&mut txs_to_add);
}
}
}
Ok(txs)
}
async fn blocks(
&self,
ctx: &Context<'_>,
last: Option<u64>,
range: Option<[u64; 2]>,
) -> FieldResult<Vec<Block>> {
match (last, range) {
(Some(count), None) => last_blocks(ctx, count).await,
(None, Some([from, to])) => blocks_range(ctx, from, to).await,
_ => Err(FieldError::new("")),
}
}
async fn mempool_txs(
&self,
ctx: &'_ Context<'_>,
) -> FieldResult<Vec<Transaction<'_>>> {
mempool(ctx).await
}
async fn mempool_tx(
&self,
ctx: &'_ Context<'_>,
hash: String,
) -> OptResult<Transaction<'_>> {
mempool_by_hash(ctx, hash).await
}
async fn last_block_pair(
&self,
ctx: &Context<'_>,
) -> FieldResult<BlockPair> {
let tip = last_block(ctx).await?;
let tip_height = tip.header().height;
let last_finalized_block =
ctx.data::<DBContext>()?.0.read().await.view(|v| {
for height in (0..=tip_height).rev() {
if let Ok(Some((hash, Label::Final(_)))) =
v.block_label_by_height(height)
{
let hash = hex::encode(hash);
return Ok((height, hash));
}
}
Err(anyhow::anyhow!(
"could not fetch the last final block by height"
))
})?;
let last_block = (tip_height, hex::encode(tip.header().hash));
Ok(BlockPair {
last_block,
last_finalized_block,
})
}
#[cfg(feature = "archive")]
async fn full_moonlight_history(
&self,
ctx: &Context<'_>,
address: String,
ord: Option<String>,
from_block: Option<u64>,
to_block: Option<u64>,
) -> OptResult<MoonlightTransfers> {
full_moonlight_history(ctx, address, ord, from_block, to_block).await
}
#[allow(clippy::too_many_arguments)]
#[cfg(feature = "archive")]
async fn moonlight_history(
&self,
ctx: &Context<'_>,
sender: Option<String>,
receiver: Option<String>,
from_block: Option<u64>,
to_block: Option<u64>,
max_count: Option<usize>,
page_count: Option<usize>,
) -> OptResult<MoonlightTransfers> {
if max_count == Some(0) {
return Err(FieldError::new("MaxCount must be greater than 0"));
}
fetch_moonlight_history(
ctx, sender, receiver, from_block, to_block, max_count, page_count,
)
.await
}
#[cfg(feature = "archive")]
async fn transactions_by_memo(
&self,
ctx: &Context<'_>,
memo: String,
) -> OptResult<MoonlightTransfers> {
let memo = memo.into_bytes();
moonlight_tx_by_memo(ctx, memo).await
}
#[cfg(feature = "archive")]
async fn contract_events(
&self,
ctx: &Context<'_>,
height: Option<i64>,
hash: Option<String>,
) -> OptResult<ContractEvents> {
match (height, hash) {
(Some(height), None) => events_by_height(ctx, height).await,
(None, Some(hash)) => events_by_hash(ctx, hash).await,
_ => Err(FieldError::new("Specify height or hash")),
}
}
#[cfg(feature = "archive")]
async fn finalized_events(
&self,
ctx: &Context<'_>,
contract_id: String,
limit: Option<i64>,
cursor: Option<String>,
) -> OptResult<ContractEvents> {
finalized_events_by_contract(ctx, contract_id, limit, cursor).await
}
async fn check_block(
&self,
ctx: &Context<'_>,
height: u64,
hash: String,
only_finalized: Option<bool>,
) -> FieldResult<bool> {
if only_finalized.unwrap_or(false) {
#[cfg(not(feature = "archive"))]
return Err(FieldError::new(
"only_finalized is supported only by archiver",
));
#[cfg(feature = "archive")]
check_finalized_block(ctx, height as i64, hash).await
} else {
check_block(ctx, height, hash).await
}
}
#[cfg(feature = "archive")]
async fn next_phoenix(
&self,
ctx: &Context<'_>,
height: i64,
) -> OptResult<u64> {
let (_, archive) = ctx.data::<DBContext>()?;
let next_height = archive.next_phoenix(height).await?;
Ok(next_height)
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_graphql::{EmptyMutation, EmptySubscription, Schema};
type TestSchema = Schema<Query, EmptyMutation, EmptySubscription>;
fn schema() -> TestSchema {
Schema::build(Query, EmptyMutation, EmptySubscription).finish()
}
#[tokio::test]
async fn over_limit_queries_return_error() {
let cases = vec![
(
"transactions(last)",
format!(
"{{ transactions(last: {}) {{ tx {{ id }} }} }}",
MAX_GRAPHQL_TXS_PER_QUERY + 1
),
MAX_GRAPHQL_TXS_PER_QUERY.to_string(),
),
(
"blocks(last)",
format!(
"{{ blocks(last: {}) {{ header {{ height }} }} }}",
MAX_GRAPHQL_BLOCKS_PER_QUERY + 1
),
MAX_GRAPHQL_BLOCKS_PER_QUERY.to_string(),
),
(
"blocks(range)",
format!(
"{{ blocks(range: [0, {}]) {{ header {{ height }} }} }}",
MAX_GRAPHQL_BLOCKS_PER_QUERY
),
MAX_GRAPHQL_BLOCKS_PER_QUERY.to_string(),
),
];
let schema = schema();
for (case_name, query, max) in cases {
let res = schema.execute(query).await;
assert!(
!res.errors.is_empty(),
"{case_name}: expected errors, got: {res:?}"
);
assert!(
res.errors[0].message.contains(&max),
"{case_name}: expected error message to mention max {max}, got: {res:?}"
);
}
}
}