use std::{borrow::Cow, iter, net::SocketAddr, num::NonZeroU16, sync::Arc};
use async_graphql::{
futures_util::Stream,
parser::types::{DocumentOperations, ExecutableDocument, OperationType},
resolver_utils::ContainerType,
Error, MergedObject, OutputType, Request, ScalarType, Schema, ServerError, SimpleObject,
Subscription,
};
use async_graphql_axum::{GraphQLRequest, GraphQLResponse, GraphQLSubscription};
use axum::{extract::Path, http::StatusCode, response, response::IntoResponse, Extension, Router};
use futures::{
future::{self},
lock::Mutex,
Future,
};
use linera_base::{
crypto::{CryptoError, CryptoHash, PublicKey},
data_types::{
Amount, ApplicationPermissions, BlobContent, Bytecode, TimeDelta, Timestamp,
UserApplicationDescription,
},
identifiers::{ApplicationId, BlobId, BytecodeId, ChainId, Owner, UserApplicationId},
ownership::{ChainOwnership, TimeoutConfig},
BcsHexParseError,
};
use linera_chain::{data_types::HashedCertificateValue, ChainStateView};
use linera_client::{
chain_clients::ChainClients,
chain_listener::{ChainListener, ChainListenerConfig, ClientContext},
};
use linera_core::{
client::{ChainClient, ChainClientError},
data_types::{ClientOutcome, RoundTimeout},
node::{NotificationStream, ValidatorNode, ValidatorNodeProvider},
worker::{Notification, Reason},
};
use linera_execution::{
committee::{Committee, Epoch},
system::{AdminOperation, Recipient, SystemChannel, UserData},
Operation, Query, Response, SystemOperation,
};
use linera_storage::Storage;
use serde::{Deserialize, Serialize};
use serde_json::json;
use thiserror::Error as ThisError;
use tokio::sync::OwnedRwLockReadGuard;
use tokio_stream::StreamExt;
use tower_http::cors::CorsLayer;
use tracing::{debug, error, info};
use crate::util;
#[derive(SimpleObject, Serialize, Deserialize, Clone)]
pub struct Chains {
pub list: Vec<ChainId>,
pub default: Option<ChainId>,
}
pub struct QueryRoot<P, S>
where
S: Storage,
{
clients: ChainClients<P, S>,
port: NonZeroU16,
default_chain: Option<ChainId>,
}
pub struct SubscriptionRoot<P, S>
where
S: Storage,
{
clients: ChainClients<P, S>,
}
pub struct MutationRoot<P, S, C>
where
S: Storage,
{
clients: ChainClients<P, S>,
context: Arc<Mutex<C>>,
}
#[derive(Debug, ThisError)]
enum NodeServiceError {
#[error(transparent)]
ChainClientError(#[from] ChainClientError),
#[error(transparent)]
BcsHexError(#[from] BcsHexParseError),
#[error("could not decode query string")]
QueryStringError(#[from] hex::FromHexError),
#[error(transparent)]
BcsError(#[from] bcs::Error),
#[error(transparent)]
JsonError(#[from] serde_json::Error),
#[error("missing GraphQL operation")]
MissingOperation,
#[error("unsupported query type: subscription")]
UnsupportedQueryType,
#[error("GraphQL operations of different types submitted")]
HeterogeneousOperations,
#[error("failed to parse GraphQL query: {error}")]
GraphQLParseError { error: String },
#[error("malformed application response")]
MalformedApplicationResponse,
#[error("application service error")]
ApplicationServiceError { errors: Vec<String> },
#[error("chain ID not found")]
UnknownChainId { chain_id: String },
#[error("malformed chain ID")]
InvalidChainId(CryptoError),
}
impl From<ServerError> for NodeServiceError {
fn from(value: ServerError) -> Self {
NodeServiceError::GraphQLParseError {
error: value.to_string(),
}
}
}
impl IntoResponse for NodeServiceError {
fn into_response(self) -> response::Response {
let tuple = match self {
NodeServiceError::BcsHexError(e) => (StatusCode::BAD_REQUEST, vec![e.to_string()]),
NodeServiceError::QueryStringError(e) => (StatusCode::BAD_REQUEST, vec![e.to_string()]),
NodeServiceError::ChainClientError(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, vec![e.to_string()])
}
NodeServiceError::BcsError(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, vec![e.to_string()])
}
NodeServiceError::JsonError(e) => {
(StatusCode::INTERNAL_SERVER_ERROR, vec![e.to_string()])
}
NodeServiceError::MalformedApplicationResponse => {
(StatusCode::INTERNAL_SERVER_ERROR, vec![self.to_string()])
}
NodeServiceError::MissingOperation
| NodeServiceError::HeterogeneousOperations
| NodeServiceError::UnsupportedQueryType => {
(StatusCode::BAD_REQUEST, vec![self.to_string()])
}
NodeServiceError::GraphQLParseError { error } => (StatusCode::BAD_REQUEST, vec![error]),
NodeServiceError::ApplicationServiceError { errors } => {
(StatusCode::BAD_REQUEST, errors)
}
NodeServiceError::UnknownChainId { chain_id } => (
StatusCode::NOT_FOUND,
vec![format!("unknown chain ID: {}", chain_id)],
),
NodeServiceError::InvalidChainId(_) => (
StatusCode::BAD_REQUEST,
vec!["invalid chain ID".to_string()],
),
};
let tuple = (tuple.0, json!({"error": tuple.1}).to_string());
tuple.into_response()
}
}
#[Subscription]
impl<P, S> SubscriptionRoot<P, S>
where
P: ValidatorNodeProvider + Send + Sync + 'static,
S: Storage + Clone + Send + Sync + 'static,
{
async fn notifications(
&self,
chain_id: ChainId,
) -> Result<impl Stream<Item = Notification>, Error> {
let client = self.clients.try_client_lock(&chain_id).await?;
Ok(client.subscribe().await?)
}
}
impl<P, S, C> MutationRoot<P, S, C>
where
P: ValidatorNodeProvider + Send + Sync + 'static,
S: Storage + Clone + Send + Sync + 'static,
C: ClientContext<ValidatorNodeProvider = P, Storage = S> + Send + 'static,
{
async fn execute_system_operation(
&self,
system_operation: SystemOperation,
chain_id: ChainId,
) -> Result<CryptoHash, Error> {
let certificate = self
.apply_client_command(&chain_id, move |client| {
let operation = Operation::System(system_operation.clone());
async move {
let result = client
.execute_operation(operation)
.await
.map_err(Error::from);
(result, client)
}
})
.await?;
Ok(certificate.hash())
}
async fn apply_client_command<F, Fut, T>(
&self,
chain_id: &ChainId,
mut f: F,
) -> Result<T, Error>
where
F: FnMut(ChainClient<P, S>) -> Fut,
Fut: Future<Output = (Result<ClientOutcome<T>, Error>, ChainClient<P, S>)>,
{
loop {
let client = self.clients.try_client_lock(chain_id).await?;
let mut stream = client.subscribe().await?;
let (result, client) = f(client).await;
self.context.lock().await.update_wallet(&client).await;
let timeout = match result? {
ClientOutcome::Committed(t) => return Ok(t),
ClientOutcome::WaitForTimeout(timeout) => timeout,
};
drop(client);
wait_for_next_round(&mut stream, timeout).await;
}
}
}
#[async_graphql::Object(cache_control(no_cache))]
impl<P, S, C> MutationRoot<P, S, C>
where
P: ValidatorNodeProvider + Send + Sync + 'static,
S: Storage + Clone + Send + Sync + 'static,
C: ClientContext<ValidatorNodeProvider = P, Storage = S> + Send + 'static,
{
async fn process_inbox(&self, chain_id: ChainId) -> Result<Vec<CryptoHash>, Error> {
let mut hashes = Vec::new();
loop {
let client = self.clients.try_client_lock(&chain_id).await?;
client.synchronize_from_validators().await?;
let result = client.process_inbox().await;
self.context.lock().await.update_wallet(&client).await;
let (certificates, maybe_timeout) = result?;
hashes.extend(certificates.into_iter().map(|cert| cert.hash()));
match maybe_timeout {
None => return Ok(hashes),
Some(timestamp) => {
let mut stream = client.subscribe().await?;
drop(client);
wait_for_next_round(&mut stream, timestamp).await;
}
}
}
}
async fn retry_pending_block(&self, chain_id: ChainId) -> Result<Option<CryptoHash>, Error> {
let client = self.clients.try_client_lock(&chain_id).await?;
let outcome = client.process_pending_block().await?;
self.context.lock().await.update_wallet(&client).await;
match outcome {
ClientOutcome::Committed(Some(certificate)) => Ok(Some(certificate.hash())),
ClientOutcome::Committed(None) => Ok(None),
ClientOutcome::WaitForTimeout(timeout) => Err(Error::from(format!(
"Please try again at {}",
timeout.timestamp
))),
}
}
async fn transfer(
&self,
chain_id: ChainId,
owner: Option<Owner>,
recipient: Recipient,
amount: Amount,
user_data: Option<UserData>,
) -> Result<CryptoHash, Error> {
self.apply_client_command(&chain_id, move |client| {
let user_data = user_data.clone();
async move {
let result = client
.transfer(owner, amount, recipient, user_data.unwrap_or_default())
.await
.map_err(Error::from)
.map(|outcome| outcome.map(|certificate| certificate.hash()));
(result, client)
}
})
.await
}
#[allow(clippy::too_many_arguments)]
async fn claim(
&self,
chain_id: ChainId,
owner: Owner,
target_id: ChainId,
recipient: Recipient,
amount: Amount,
user_data: Option<UserData>,
) -> Result<CryptoHash, Error> {
self.apply_client_command(&chain_id, move |client| {
let user_data = user_data.clone();
async move {
let result = client
.claim(
owner,
target_id,
recipient,
amount,
user_data.unwrap_or_default(),
)
.await
.map_err(Error::from)
.map(|outcome| outcome.map(|certificate| certificate.hash()));
(result, client)
}
})
.await
}
#[allow(clippy::too_many_arguments)]
async fn read_data_blob(
&self,
chain_id: ChainId,
hash: CryptoHash,
) -> Result<CryptoHash, Error> {
self.apply_client_command(&chain_id, move |client| async move {
let result = client
.read_data_blob(hash)
.await
.map_err(Error::from)
.map(|outcome| outcome.map(|certificate| certificate.hash()));
(result, client)
})
.await
}
async fn open_chain(
&self,
chain_id: ChainId,
public_key: PublicKey,
balance: Option<Amount>,
) -> Result<ChainId, Error> {
let ownership = ChainOwnership::single(public_key);
let balance = balance.unwrap_or(Amount::ZERO);
let message_id = self
.apply_client_command(&chain_id, move |client| {
let ownership = ownership.clone();
async move {
let result = client
.open_chain(ownership, ApplicationPermissions::default(), balance)
.await
.map_err(Error::from)
.map(|outcome| outcome.map(|(message_id, _)| message_id));
(result, client)
}
})
.await?;
Ok(ChainId::child(message_id))
}
#[allow(clippy::too_many_arguments)]
async fn open_multi_owner_chain(
&self,
chain_id: ChainId,
application_permissions: Option<ApplicationPermissions>,
public_keys: Vec<PublicKey>,
weights: Option<Vec<u64>>,
multi_leader_rounds: Option<u32>,
balance: Option<Amount>,
#[graphql(desc = "The duration of the fast round, in milliseconds; default: no timeout")]
fast_round_ms: Option<u64>,
#[graphql(
desc = "The duration of the first single-leader and all multi-leader rounds",
default = 10_000
)]
base_timeout_ms: u64,
#[graphql(
desc = "The number of milliseconds by which the timeout increases after each \
single-leader round",
default = 1_000
)]
timeout_increment_ms: u64,
#[graphql(
desc = "The age of an incoming tracked or protected message after which the \
validators start transitioning the chain to fallback mode, in milliseconds.",
default = 86_400_000
)]
fallback_duration_ms: u64,
) -> Result<ChainId, Error> {
let owners = if let Some(weights) = weights {
if weights.len() != public_keys.len() {
return Err(Error::new(format!(
"There are {} public keys but {} weights.",
public_keys.len(),
weights.len()
)));
}
public_keys.into_iter().zip(weights).collect::<Vec<_>>()
} else {
public_keys
.into_iter()
.zip(iter::repeat(100))
.collect::<Vec<_>>()
};
let multi_leader_rounds = multi_leader_rounds.unwrap_or(u32::MAX);
let timeout_config = TimeoutConfig {
fast_round_duration: fast_round_ms.map(TimeDelta::from_millis),
base_timeout: TimeDelta::from_millis(base_timeout_ms),
timeout_increment: TimeDelta::from_millis(timeout_increment_ms),
fallback_duration: TimeDelta::from_millis(fallback_duration_ms),
};
let ownership = ChainOwnership::multiple(owners, multi_leader_rounds, timeout_config);
let balance = balance.unwrap_or(Amount::ZERO);
let message_id = self
.apply_client_command(&chain_id, move |client| {
let ownership = ownership.clone();
let application_permissions = application_permissions.clone().unwrap_or_default();
async move {
let result = client
.open_chain(ownership, application_permissions, balance)
.await
.map_err(Error::from)
.map(|outcome| outcome.map(|(message_id, _)| message_id));
(result, client)
}
})
.await?;
Ok(ChainId::child(message_id))
}
async fn close_chain(&self, chain_id: ChainId) -> Result<CryptoHash, Error> {
let certificate = self
.apply_client_command(&chain_id, |client| async move {
let result = client.close_chain().await.map_err(Error::from);
(result, client)
})
.await?;
Ok(certificate.hash())
}
async fn change_owner(
&self,
chain_id: ChainId,
new_public_key: PublicKey,
) -> Result<CryptoHash, Error> {
let operation = SystemOperation::ChangeOwnership {
super_owners: vec![new_public_key],
owners: Vec::new(),
multi_leader_rounds: 2,
timeout_config: TimeoutConfig::default(),
};
self.execute_system_operation(operation, chain_id).await
}
#[allow(clippy::too_many_arguments)]
async fn change_multiple_owners(
&self,
chain_id: ChainId,
new_public_keys: Vec<PublicKey>,
new_weights: Vec<u64>,
multi_leader_rounds: u32,
#[graphql(desc = "The duration of the fast round, in milliseconds; default: no timeout")]
fast_round_ms: Option<u64>,
#[graphql(
desc = "The duration of the first single-leader and all multi-leader rounds",
default = 10_000
)]
base_timeout_ms: u64,
#[graphql(
desc = "The number of milliseconds by which the timeout increases after each \
single-leader round",
default = 1_000
)]
timeout_increment_ms: u64,
#[graphql(
desc = "The age of an incoming tracked or protected message after which the \
validators start transitioning the chain to fallback mode, in milliseconds.",
default = 86_400_000
)]
fallback_duration_ms: u64,
) -> Result<CryptoHash, Error> {
let operation = SystemOperation::ChangeOwnership {
super_owners: Vec::new(),
owners: new_public_keys.into_iter().zip(new_weights).collect(),
multi_leader_rounds,
timeout_config: TimeoutConfig {
fast_round_duration: fast_round_ms.map(TimeDelta::from_millis),
base_timeout: TimeDelta::from_millis(base_timeout_ms),
timeout_increment: TimeDelta::from_millis(timeout_increment_ms),
fallback_duration: TimeDelta::from_millis(fallback_duration_ms),
},
};
self.execute_system_operation(operation, chain_id).await
}
async fn change_application_permissions(
&self,
chain_id: ChainId,
close_chain: Vec<ApplicationId>,
execute_operations: Option<Vec<ApplicationId>>,
mandatory_applications: Vec<ApplicationId>,
) -> Result<CryptoHash, Error> {
let operation = SystemOperation::ChangeApplicationPermissions(ApplicationPermissions {
execute_operations,
mandatory_applications,
close_chain,
});
self.execute_system_operation(operation, chain_id).await
}
async fn create_committee(
&self,
chain_id: ChainId,
epoch: Epoch,
committee: Committee,
) -> Result<CryptoHash, Error> {
let operation =
SystemOperation::Admin(AdminOperation::CreateCommittee { epoch, committee });
self.execute_system_operation(operation, chain_id).await
}
async fn subscribe(
&self,
subscriber_chain_id: ChainId,
publisher_chain_id: ChainId,
channel: SystemChannel,
) -> Result<CryptoHash, Error> {
let operation = SystemOperation::Subscribe {
chain_id: publisher_chain_id,
channel,
};
self.execute_system_operation(operation, subscriber_chain_id)
.await
}
async fn unsubscribe(
&self,
subscriber_chain_id: ChainId,
publisher_chain_id: ChainId,
channel: SystemChannel,
) -> Result<CryptoHash, Error> {
let operation = SystemOperation::Unsubscribe {
chain_id: publisher_chain_id,
channel,
};
self.execute_system_operation(operation, subscriber_chain_id)
.await
}
async fn remove_committee(&self, chain_id: ChainId, epoch: Epoch) -> Result<CryptoHash, Error> {
let operation = SystemOperation::Admin(AdminOperation::RemoveCommittee { epoch });
self.execute_system_operation(operation, chain_id).await
}
async fn publish_bytecode(
&self,
chain_id: ChainId,
contract: Bytecode,
service: Bytecode,
) -> Result<BytecodeId, Error> {
self.apply_client_command(&chain_id, move |client| {
let contract = contract.clone();
let service = service.clone();
async move {
let result = client
.publish_bytecode(contract, service)
.await
.map_err(Error::from)
.map(|outcome| outcome.map(|(bytecode_id, _)| bytecode_id));
(result, client)
}
})
.await
}
async fn publish_data_blob(
&self,
chain_id: ChainId,
blob_content: BlobContent,
) -> Result<BlobId, Error> {
let blob_id = BlobId::new_data(&blob_content);
self.apply_client_command(&chain_id, move |client| {
let blob_content = blob_content.clone();
async move {
let result = client
.publish_data_blob(blob_content)
.await
.map_err(Error::from)
.map(|outcome| outcome.map(|_| blob_id));
(result, client)
}
})
.await
}
async fn create_application(
&self,
chain_id: ChainId,
bytecode_id: BytecodeId,
parameters: String,
instantiation_argument: String,
required_application_ids: Vec<UserApplicationId>,
) -> Result<ApplicationId, Error> {
self.apply_client_command(&chain_id, move |client| {
let parameters = parameters.as_bytes().to_vec();
let instantiation_argument = instantiation_argument.as_bytes().to_vec();
let required_application_ids = required_application_ids.clone();
async move {
let result = client
.create_application_untyped(
bytecode_id,
parameters,
instantiation_argument,
required_application_ids,
)
.await
.map_err(Error::from)
.map(|outcome| outcome.map(|(application_id, _)| application_id));
(result, client)
}
})
.await
}
async fn request_application(
&self,
chain_id: ChainId,
application_id: UserApplicationId,
target_chain_id: Option<ChainId>,
) -> Result<CryptoHash, Error> {
loop {
let client = self.clients.try_client_lock(&chain_id).await?;
let result = client
.request_application(application_id, target_chain_id)
.await;
self.context.lock().await.update_wallet(&client).await;
let timeout = match result? {
ClientOutcome::Committed(certificate) => return Ok(certificate.hash()),
ClientOutcome::WaitForTimeout(timeout) => timeout,
};
let mut stream = client.subscribe().await?;
drop(client);
wait_for_next_round(&mut stream, timeout).await;
}
}
}
#[async_graphql::Object(cache_control(no_cache))]
impl<P, S> QueryRoot<P, S>
where
P: ValidatorNodeProvider + Send + Sync + 'static,
S: Storage + Clone + Send + Sync + 'static,
{
async fn chain(&self, chain_id: ChainId) -> Result<ChainStateExtendedView<S::Context>, Error> {
let client = self.clients.try_client_lock(&chain_id).await?;
let view = client.chain_state_view().await?;
Ok(ChainStateExtendedView::new(view))
}
async fn applications(&self, chain_id: ChainId) -> Result<Vec<ApplicationOverview>, Error> {
let client = self.clients.try_client_lock(&chain_id).await?;
let applications = client
.chain_state_view()
.await?
.execution_state
.list_applications()
.await?;
let overviews = applications
.into_iter()
.map(|(id, description)| ApplicationOverview::new(id, description, self.port, chain_id))
.collect();
Ok(overviews)
}
async fn chains(&self) -> Result<Chains, Error> {
Ok(Chains {
list: self.clients.0.lock().await.keys().cloned().collect(),
default: self.default_chain,
})
}
async fn block(
&self,
hash: Option<CryptoHash>,
chain_id: ChainId,
) -> Result<Option<HashedCertificateValue>, Error> {
let client = self.clients.try_client_lock(&chain_id).await?;
let hash = match hash {
Some(hash) => Some(hash),
None => {
let view = client.chain_state_view().await?;
view.tip_state.get().block_hash
}
};
if let Some(hash) = hash {
let block = client.read_hashed_certificate_value(hash).await?;
Ok(Some(block))
} else {
Ok(None)
}
}
async fn blocks(
&self,
from: Option<CryptoHash>,
chain_id: ChainId,
limit: Option<u32>,
) -> Result<Vec<HashedCertificateValue>, Error> {
let client = self.clients.try_client_lock(&chain_id).await?;
let limit = limit.unwrap_or(10);
let from = match from {
Some(from) => Some(from),
None => {
let view = client.chain_state_view().await?;
view.tip_state.get().block_hash
}
};
if let Some(from) = from {
let values = client
.read_hashed_certificate_values_downward(from, limit)
.await?;
Ok(values)
} else {
Ok(vec![])
}
}
async fn version(&self) -> linera_version::VersionInfo {
linera_version::VersionInfo::default()
}
}
struct ChainStateViewExtension(ChainId);
#[async_graphql::Object(cache_control(no_cache))]
impl ChainStateViewExtension {
async fn chain_id(&self) -> ChainId {
self.0
}
}
#[derive(MergedObject)]
struct ChainStateExtendedView<C>(ChainStateViewExtension, ReadOnlyChainStateView<C>)
where
C: linera_views::context::Context + Clone + Send + Sync + 'static,
C::Extra: linera_execution::ExecutionRuntimeContext;
pub struct ReadOnlyChainStateView<C>(OwnedRwLockReadGuard<ChainStateView<C>>)
where
C: linera_views::context::Context + Clone + Send + Sync + 'static;
impl<C> ContainerType for ReadOnlyChainStateView<C>
where
C: linera_views::context::Context + Clone + Send + Sync + 'static,
{
async fn resolve_field(
&self,
context: &async_graphql::Context<'_>,
) -> async_graphql::ServerResult<Option<async_graphql::Value>> {
self.0.resolve_field(context).await
}
}
impl<C> OutputType for ReadOnlyChainStateView<C>
where
C: linera_views::context::Context + Clone + Send + Sync + 'static,
{
fn type_name() -> Cow<'static, str> {
ChainStateView::<C>::type_name()
}
fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String {
ChainStateView::<C>::create_type_info(registry)
}
async fn resolve(
&self,
context: &async_graphql::ContextSelectionSet<'_>,
field: &async_graphql::Positioned<async_graphql::parser::types::Field>,
) -> async_graphql::ServerResult<async_graphql::Value> {
self.0.resolve(context, field).await
}
}
impl<C> ChainStateExtendedView<C>
where
C: linera_views::context::Context + Clone + Send + Sync + 'static,
C::Extra: linera_execution::ExecutionRuntimeContext,
{
fn new(view: OwnedRwLockReadGuard<ChainStateView<C>>) -> Self {
Self(
ChainStateViewExtension(view.chain_id()),
ReadOnlyChainStateView(view),
)
}
}
#[derive(SimpleObject)]
pub struct ApplicationOverview {
id: UserApplicationId,
description: UserApplicationDescription,
link: String,
}
impl ApplicationOverview {
fn new(
id: UserApplicationId,
description: UserApplicationDescription,
port: NonZeroU16,
chain_id: ChainId,
) -> Self {
Self {
id,
description,
link: format!(
"http://localhost:{}/chains/{}/applications/{}",
port.get(),
chain_id,
id
),
}
}
}
fn operation_type(document: &ExecutableDocument) -> Result<OperationType, NodeServiceError> {
match &document.operations {
DocumentOperations::Single(op) => Ok(op.node.ty),
DocumentOperations::Multiple(ops) => {
let mut op_types = ops.values().map(|v| v.node.ty);
let first = op_types.next().ok_or(NodeServiceError::MissingOperation)?;
op_types
.all(|x| x == first)
.then_some(first)
.ok_or(NodeServiceError::HeterogeneousOperations)
}
}
}
fn bytes_from_response(data: async_graphql::Value) -> Vec<Vec<u8>> {
if let async_graphql::Value::Object(map) = data {
map.values()
.filter_map(|value| {
if let async_graphql::Value::List(list) = value {
bytes_from_list(list)
} else {
None
}
})
.collect()
} else {
vec![]
}
}
fn bytes_from_list(list: &[async_graphql::Value]) -> Option<Vec<u8>> {
list.iter()
.map(|item| {
if let async_graphql::Value::Number(n) = item {
n.as_u64().map(|n| n as u8)
} else {
None
}
})
.collect()
}
pub struct NodeService<P, S, C>
where
S: Storage,
{
clients: ChainClients<P, S>,
config: ChainListenerConfig,
port: NonZeroU16,
default_chain: Option<ChainId>,
storage: S,
context: Arc<Mutex<C>>,
}
impl<P, S: Clone, C> Clone for NodeService<P, S, C>
where
S: Storage,
{
fn clone(&self) -> Self {
Self {
clients: self.clients.clone(),
config: self.config.clone(),
port: self.port,
default_chain: self.default_chain,
storage: self.storage.clone(),
context: self.context.clone(),
}
}
}
impl<P, S, C> NodeService<P, S, C>
where
P: ValidatorNodeProvider + Send + Sync + 'static,
<<P as ValidatorNodeProvider>::Node as ValidatorNode>::NotificationStream: Send,
S: Storage + Clone + Send + Sync + 'static,
C: ClientContext<ValidatorNodeProvider = P, Storage = S> + Send + 'static,
{
pub fn new(
config: ChainListenerConfig,
port: NonZeroU16,
default_chain: Option<ChainId>,
storage: S,
context: C,
) -> Self {
Self {
clients: ChainClients::default(),
config,
port,
default_chain,
storage,
context: Arc::new(Mutex::new(context)),
}
}
#[allow(clippy::type_complexity)]
pub fn schema(&self) -> Schema<QueryRoot<P, S>, MutationRoot<P, S, C>, SubscriptionRoot<P, S>> {
Schema::build(
QueryRoot {
clients: self.clients.clone(),
port: self.port,
default_chain: self.default_chain,
},
MutationRoot {
clients: self.clients.clone(),
context: self.context.clone(),
},
SubscriptionRoot {
clients: self.clients.clone(),
},
)
.finish()
}
#[tracing::instrument(name = "node_service", level = "info", skip(self), fields(port = ?self.port))]
pub async fn run(self) -> Result<(), anyhow::Error> {
let port = self.port.get();
let index_handler = axum::routing::get(util::graphiql).post(Self::index_handler);
let application_handler =
axum::routing::get(util::graphiql).post(Self::application_handler);
let app = Router::new()
.route("/", index_handler)
.route(
"/chains/:chain_id/applications/:application_id",
application_handler,
)
.route("/ready", axum::routing::get(|| async { "ready!" }))
.route_service("/ws", GraphQLSubscription::new(self.schema()))
.layer(Extension(self.clone()))
.layer(CorsLayer::permissive());
info!("GraphiQL IDE: http://localhost:{}", port);
ChainListener::new(self.config, self.clients.clone())
.run(self.context.clone(), self.storage.clone())
.await;
let serve_fut = axum::serve(
tokio::net::TcpListener::bind(SocketAddr::from(([127, 0, 0, 1], port))).await?,
app,
);
serve_fut.await?;
Ok(())
}
async fn user_application_query(
&self,
application_id: UserApplicationId,
request: &Request,
chain_id: ChainId,
) -> Result<async_graphql::Response, NodeServiceError> {
let bytes = serde_json::to_vec(&request)?;
let query = Query::User {
application_id,
bytes,
};
let Some(client) = self.clients.client_lock(&chain_id).await else {
return Err(NodeServiceError::UnknownChainId {
chain_id: chain_id.to_string(),
});
};
let response = client.query_application(query).await?;
let user_response_bytes = match response {
Response::System(_) => unreachable!("cannot get a system response for a user query"),
Response::User(user) => user,
};
Ok(serde_json::from_slice(&user_response_bytes)?)
}
async fn user_application_mutation(
&self,
application_id: UserApplicationId,
request: &Request,
chain_id: ChainId,
) -> Result<async_graphql::Response, NodeServiceError> {
debug!("Request: {:?}", &request);
let graphql_response = self
.user_application_query(application_id, request, chain_id)
.await?;
if graphql_response.is_err() {
let errors = graphql_response
.errors
.iter()
.map(|e| e.to_string())
.collect();
return Err(NodeServiceError::ApplicationServiceError { errors });
}
debug!("Response: {:?}", &graphql_response);
let bcs_bytes_list = bytes_from_response(graphql_response.data);
if bcs_bytes_list.is_empty() {
return Err(NodeServiceError::MalformedApplicationResponse);
}
let operations = bcs_bytes_list
.into_iter()
.map(|bytes| Operation::User {
application_id,
bytes,
})
.collect::<Vec<_>>();
let hash = loop {
let Some(client) = self.clients.client_lock(&chain_id).await else {
return Err(NodeServiceError::UnknownChainId {
chain_id: chain_id.to_string(),
});
};
let timeout = match client.execute_operations(operations.clone()).await? {
ClientOutcome::Committed(certificate) => break certificate.value.hash(),
ClientOutcome::WaitForTimeout(timeout) => timeout,
};
let mut stream = client.subscribe().await.map_err(|_| {
ChainClientError::InternalError("Could not subscribe to the local node.")
})?;
drop(client);
wait_for_next_round(&mut stream, timeout).await;
};
Ok(async_graphql::Response::new(hash.to_value()))
}
async fn index_handler(service: Extension<Self>, request: GraphQLRequest) -> GraphQLResponse {
service
.0
.schema()
.execute(request.into_inner())
.await
.into()
}
async fn application_handler(
Path((chain_id, application_id)): Path<(String, String)>,
service: Extension<Self>,
request: GraphQLRequest,
) -> Result<GraphQLResponse, NodeServiceError> {
let mut request = request.into_inner();
let parsed_query = request.parsed_query()?;
let operation_type = operation_type(parsed_query)?;
let chain_id: ChainId = chain_id.parse().map_err(NodeServiceError::InvalidChainId)?;
let application_id: UserApplicationId = application_id.parse()?;
let response = match operation_type {
OperationType::Query => {
service
.0
.user_application_query(application_id, &request, chain_id)
.await?
}
OperationType::Mutation => {
service
.0
.user_application_mutation(application_id, &request, chain_id)
.await?
}
OperationType::Subscription => return Err(NodeServiceError::UnsupportedQueryType),
};
Ok(response.into())
}
}
pub async fn wait_for_next_round(stream: &mut NotificationStream, timeout: RoundTimeout) {
let mut stream = stream.filter(|notification| match ¬ification.reason {
Reason::NewBlock { height, .. } => *height >= timeout.next_block_height,
Reason::NewRound { round, .. } => *round > timeout.current_round,
Reason::NewIncomingBundle { .. } => false,
});
future::select(
Box::pin(stream.next()),
Box::pin(tokio::time::sleep(
timeout.timestamp.duration_since(Timestamp::now()),
)),
)
.await;
}