openmina_node_native/graphql/
mod.rs

1use account::{create_account_loader, AccountLoader, GraphQLAccount};
2use block::{GraphQLBlock, GraphQLSnarkJob, GraphQLUserCommands};
3use juniper::{graphql_value, EmptySubscription, FieldError, GraphQLEnum, RootNode};
4use ledger::{Account, AccountId};
5use mina_p2p_messages::v2::{
6    conv, LedgerHash, MinaBaseSignedCommandStableV2, MinaBaseUserCommandStableV2,
7    MinaBaseZkappCommandTStableV1WireStableV1, TokenIdKeyHash, TransactionHash,
8};
9use mina_signer::CompressedPubKey;
10use node::{
11    account::AccountPublicKey,
12    ledger::read::LedgerStatus,
13    rpc::{
14        AccountQuery, GetBlockQuery, PooledCommandsQuery, RpcBestChainResponse,
15        RpcGenesisBlockResponse, RpcGetBlockResponse, RpcLedgerAccountDelegatorsGetResponse,
16        RpcLedgerStatusGetResponse, RpcNodeStatus, RpcPooledUserCommandsResponse,
17        RpcPooledZkappCommandsResponse, RpcRequest, RpcSnarkPoolCompletedJobsResponse,
18        RpcSnarkPoolPendingJobsGetResponse, RpcSnarkerConfig, RpcStatusGetResponse,
19        RpcSyncStatsGetResponse, RpcTransactionInjectResponse, RpcTransactionStatusGetResponse,
20        SyncStatsQuery,
21    },
22    stats::sync::SyncKind,
23    BuildEnv,
24};
25use o1_utils::field_helpers::FieldHelpersError;
26use openmina_core::{
27    block::AppliedBlock, consensus::ConsensusConstants, constants::constraint_constants,
28    NetworkConfig,
29};
30use openmina_node_common::rpc::RpcSender;
31use snark::{GraphQLPendingSnarkWork, GraphQLSnarkWorker};
32use std::str::FromStr;
33use tokio::sync::OnceCell;
34use transaction::GraphQLTransactionStatus;
35use warp::{Filter, Rejection, Reply};
36use zkapp::GraphQLZkapp;
37
38pub mod account;
39pub mod block;
40pub mod constants;
41pub mod snark;
42pub mod transaction;
43pub mod user_command;
44pub mod zkapp;
45
46/// Base58 encoded public key
47pub type GraphQLPublicKey = String;
48
49#[derive(Debug, thiserror::Error)]
50pub enum Error {
51    #[error("Conversion error: {0}")]
52    Conversion(ConversionError),
53    #[error("State machine empty response")]
54    StateMachineEmptyResponse,
55    #[error("Custom: {0}")]
56    Custom(String),
57}
58
59#[derive(Debug, thiserror::Error)]
60pub enum ConversionError {
61    #[error(transparent)]
62    Io(#[from] std::io::Error),
63    #[error(transparent)]
64    Conversion(#[from] mina_p2p_messages::v2::conv::Error),
65    #[error("Wrong variant")]
66    WrongVariant,
67    #[error("SerdeJson: {0}")]
68    SerdeJson(#[from] serde_json::Error),
69    #[error("Base58Check: {0}")]
70    Base58Check(#[from] mina_p2p_messages::b58::FromBase58CheckError),
71    #[error("Base58 error: {0}")]
72    Base58(#[from] bs58::decode::Error),
73    #[error(transparent)]
74    InvalidDecimalNumber(#[from] mina_p2p_messages::bigint::InvalidDecimalNumber),
75    #[error("Invalid bigint")]
76    InvalidBigInt,
77    #[error("Invalid hex")]
78    InvalidHex,
79    #[error(transparent)]
80    ParseInt(#[from] std::num::ParseIntError),
81    #[error(transparent)]
82    EnumParse(#[from] strum::ParseError),
83    #[error(transparent)]
84    TryFromInt(#[from] std::num::TryFromIntError),
85    #[error("Missing field: {0}")]
86    MissingField(String),
87    #[error("Invalid length")]
88    InvalidLength,
89    #[error("Custom: {0}")]
90    Custom(String),
91    #[error(transparent)]
92    FieldHelpers(#[from] FieldHelpersError),
93    #[error("Failed to convert integer to i32")]
94    Integer,
95}
96
97impl From<ConversionError> for Error {
98    fn from(value: ConversionError) -> Self {
99        Error::Conversion(value)
100    }
101}
102
103/// Context for the GraphQL API
104///
105/// This is used to share state between the GraphQL queries and mutations.
106///
107/// The caching used here is only valid for the lifetime of the context
108/// i.e. for one request which is the goal as we can have multiple sources for one request.
109/// This optimizes the number of request to the state machine
110pub(crate) struct Context {
111    rpc_sender: RpcSender,
112    account_loader: AccountLoader,
113    // Caches
114    statemachine_status_cache: OnceCell<Option<RpcNodeStatus>>,
115    best_tip_cache: OnceCell<Option<AppliedBlock>>,
116    ledger_status_cache: OnceCell<Option<LedgerStatus>>,
117}
118
119impl juniper::Context for Context {}
120
121impl Context {
122    pub fn new(rpc_sender: RpcSender) -> Self {
123        Self {
124            rpc_sender: rpc_sender.clone(),
125            statemachine_status_cache: OnceCell::new(),
126            best_tip_cache: OnceCell::new(),
127            ledger_status_cache: OnceCell::new(),
128            account_loader: create_account_loader(rpc_sender.clone()),
129        }
130    }
131
132    pub(crate) async fn get_or_fetch_status(&self) -> RpcStatusGetResponse {
133        self.statemachine_status_cache
134            .get_or_init(|| async {
135                self.rpc_sender
136                    .oneshot_request(RpcRequest::StatusGet)
137                    .await
138                    .flatten()
139            })
140            .await
141            .clone()
142    }
143
144    pub(crate) async fn get_or_fetch_best_tip(&self) -> Option<AppliedBlock> {
145        self.best_tip_cache
146            .get_or_init(|| async {
147                self.rpc_sender
148                    .oneshot_request(RpcRequest::BestChain(1))
149                    .await
150                    .and_then(|blocks: RpcBestChainResponse| blocks.first().cloned())
151            })
152            .await
153            .clone()
154    }
155
156    pub(crate) async fn get_or_fetch_ledger_status(
157        &self,
158        ledger_hash: &LedgerHash,
159    ) -> RpcLedgerStatusGetResponse {
160        self.ledger_status_cache
161            .get_or_init(|| async {
162                self.rpc_sender
163                    .oneshot_request(RpcRequest::LedgerStatusGet(ledger_hash.clone()))
164                    .await
165                    .flatten()
166            })
167            .await
168            .clone()
169    }
170
171    pub(crate) async fn load_account(&self, account_id: AccountId) -> Option<GraphQLAccount> {
172        self.account_loader.try_load(account_id).await.ok()?.ok()
173    }
174
175    pub async fn fetch_delegators(
176        &self,
177        ledger_hash: LedgerHash,
178        account_id: AccountId,
179    ) -> RpcLedgerAccountDelegatorsGetResponse {
180        self.rpc_sender
181            .oneshot_request(RpcRequest::LedgerAccountDelegatorsGet(
182                ledger_hash.clone(),
183                account_id.clone(),
184            ))
185            .await
186            .flatten()
187    }
188}
189
190#[derive(Clone, Copy, Debug, GraphQLEnum)]
191#[allow(clippy::upper_case_acronyms)]
192enum SyncStatus {
193    CONNECTING,
194    LISTENING,
195    OFFLINE,
196    BOOTSTRAP,
197    SYNCED,
198    CATCHUP,
199}
200
201#[derive(Clone, Debug)]
202struct ProtocolState {
203    consensus_state: ConsensusState,
204    blockchain_state: BlockchainState,
205}
206
207#[juniper::graphql_object(context = Context)]
208impl ProtocolState {
209    fn consensus_state(&self) -> &ConsensusState {
210        &self.consensus_state
211    }
212
213    fn blockchain_state(&self) -> &BlockchainState {
214        &self.blockchain_state
215    }
216}
217
218#[derive(Clone, Debug)]
219struct ConsensusState {
220    block_height: i32,
221}
222
223#[juniper::graphql_object(context = Context)]
224impl ConsensusState {
225    fn block_height(&self) -> i32 {
226        self.block_height
227    }
228}
229
230#[derive(Clone, Debug)]
231struct BlockchainState {
232    snarked_ledger_hash: String,
233}
234
235#[juniper::graphql_object(context = Context)]
236impl BlockchainState {
237    fn snarked_ledger_hash(&self) -> &str {
238        &self.snarked_ledger_hash
239    }
240}
241
242#[derive(Clone, Debug)]
243struct BestChain {
244    state_hash: String,
245    protocol_state: ProtocolState,
246}
247
248#[juniper::graphql_object(context = Context)]
249impl BestChain {
250    fn state_hash(&self) -> &str {
251        &self.state_hash
252    }
253
254    fn protocol_state(&self) -> &ProtocolState {
255        &self.protocol_state
256    }
257}
258
259#[derive(Clone, Copy, Debug)]
260struct Query;
261
262#[juniper::graphql_object(context = Context)]
263impl Query {
264    async fn account(
265        public_key: String,
266        token: Option<String>,
267        context: &Context,
268    ) -> juniper::FieldResult<account::GraphQLAccount> {
269        let public_key = AccountPublicKey::from_str(&public_key)?;
270        let req = match token {
271            None => AccountQuery::SinglePublicKey(public_key),
272            Some(token) => {
273                let token_id = TokenIdKeyHash::from_str(&token)?;
274                AccountQuery::PubKeyWithTokenId(public_key, token_id)
275            }
276        };
277        let accounts: Vec<Account> = context
278            .rpc_sender
279            .oneshot_request(RpcRequest::LedgerAccountsGet(req))
280            .await
281            .ok_or(Error::StateMachineEmptyResponse)?;
282
283        Ok(accounts
284            .first()
285            .cloned()
286            .ok_or(Error::StateMachineEmptyResponse)?
287            .try_into()?)
288    }
289
290    async fn sync_status(context: &Context) -> juniper::FieldResult<SyncStatus> {
291        let state: RpcSyncStatsGetResponse = context
292            .rpc_sender
293            .oneshot_request(RpcRequest::SyncStatsGet(SyncStatsQuery { limit: Some(1) }))
294            .await
295            .ok_or(Error::StateMachineEmptyResponse)?;
296
297        if let Some(state) = state.as_ref().and_then(|s| s.first()) {
298            if state.synced.is_some() {
299                Ok(SyncStatus::SYNCED)
300            } else {
301                match &state.kind {
302                    SyncKind::Bootstrap => Ok(SyncStatus::BOOTSTRAP),
303                    SyncKind::Catchup => Ok(SyncStatus::CATCHUP),
304                }
305            }
306        } else {
307            Ok(SyncStatus::LISTENING)
308        }
309    }
310
311    async fn best_chain(
312        max_length: i32,
313        context: &Context,
314    ) -> juniper::FieldResult<Vec<GraphQLBlock>> {
315        let best_chain: Vec<AppliedBlock> = context
316            .rpc_sender
317            .oneshot_request(RpcRequest::BestChain(max_length as u32))
318            .await
319            .ok_or(Error::StateMachineEmptyResponse)?;
320
321        Ok(best_chain
322            .into_iter()
323            .map(|v| v.try_into())
324            .collect::<Result<Vec<_>, _>>()?)
325    }
326
327    async fn daemon_status(
328        _context: &Context,
329    ) -> juniper::FieldResult<constants::GraphQLDaemonStatus> {
330        Ok(constants::GraphQLDaemonStatus)
331    }
332
333    async fn genesis_constants(
334        context: &Context,
335    ) -> juniper::FieldResult<constants::GraphQLGenesisConstants> {
336        let consensus_constants: ConsensusConstants = context
337            .rpc_sender
338            .oneshot_request(RpcRequest::ConsensusConstantsGet)
339            .await
340            .ok_or(Error::StateMachineEmptyResponse)?;
341        let constraint_constants = constraint_constants();
342
343        Ok(constants::GraphQLGenesisConstants::try_new(
344            constraint_constants.clone(),
345            consensus_constants,
346        )?)
347    }
348
349    async fn transaction_status(
350        payment: Option<String>,
351        zkapp_transaction: Option<String>,
352        context: &Context,
353    ) -> juniper::FieldResult<GraphQLTransactionStatus> {
354        if payment.is_some() && zkapp_transaction.is_some() {
355            return Err(Error::Custom(
356                "Cannot provide both payment and zkapp transaction".to_string(),
357            )
358            .into());
359        }
360
361        let tx = if let Some(payment) = payment {
362            MinaBaseUserCommandStableV2::SignedCommand(MinaBaseSignedCommandStableV2::from_base64(
363                &payment,
364            )?)
365        } else if let Some(zkapp_transaction) = zkapp_transaction {
366            MinaBaseUserCommandStableV2::ZkappCommand(
367                MinaBaseZkappCommandTStableV1WireStableV1::from_base64(&zkapp_transaction)?,
368            )
369        } else {
370            return Err(Error::Custom(
371                "Must provide either payment or zkapp transaction".to_string(),
372            )
373            .into());
374        };
375        let res: RpcTransactionStatusGetResponse = context
376            .rpc_sender
377            .oneshot_request(RpcRequest::TransactionStatusGet(tx))
378            .await
379            .ok_or(Error::StateMachineEmptyResponse)?;
380
381        Ok(GraphQLTransactionStatus::from(res))
382    }
383
384    /// Retrieve a block with the given state hash or height, if contained in the transition frontier
385    async fn block(
386        height: Option<i32>,
387        state_hash: Option<String>,
388        context: &Context,
389    ) -> juniper::FieldResult<GraphQLBlock> {
390        let query = match (height, state_hash) {
391            (Some(height), None) => GetBlockQuery::Height(height.try_into().unwrap_or(u32::MAX)),
392            (None, Some(state_hash)) => GetBlockQuery::Hash(state_hash.parse()?),
393            _ => {
394                return Err(Error::Custom(
395                    "Must provide exactly one of state hash, height".to_owned(),
396                )
397                .into());
398            }
399        };
400
401        let res: Option<RpcGetBlockResponse> = context
402            .rpc_sender
403            .oneshot_request(RpcRequest::GetBlock(query.clone()))
404            .await;
405
406        match res {
407            None => Err(Error::Custom("response channel dropped".to_owned()).into()),
408            Some(None) => match query {
409                GetBlockQuery::Hash(hash) => Err(Error::Custom(format!(
410                    "Could not find block with hash: `{}` in transition frontier",
411                    hash
412                ))
413                .into()),
414                GetBlockQuery::Height(height) => Err(Error::Custom(format!(
415                    "Could not find block with height: `{}` in transition frontier",
416                    height
417                ))
418                .into()),
419            },
420            Some(Some(block)) => Ok(GraphQLBlock::try_from(block)?),
421        }
422    }
423
424    /// Retrieve all the scheduled user commands for a specified sender that
425    /// the current daemon sees in its transaction pool. All scheduled
426    /// commands are queried if no sender is specified
427    ///
428    /// Arguments:
429    /// - `public_key`: base58 encoded [`AccountPublicKey`]
430    /// - `hashes`: list of base58 encoded [`TransactionHash`]es
431    /// - `ids`: list of base64 encoded [`MinaBaseZkappCommandTStableV1WireStableV1`]
432    async fn pooled_user_commands(
433        &self,
434        public_key: Option<String>,
435        hashes: Option<Vec<String>>,
436        ids: Option<Vec<String>>,
437        context: &Context,
438    ) -> juniper::FieldResult<Vec<GraphQLUserCommands>> {
439        let query = parse_pooled_commands_query(
440            public_key,
441            hashes,
442            ids,
443            MinaBaseSignedCommandStableV2::from_base64,
444        )?;
445
446        let res: RpcPooledUserCommandsResponse = context
447            .rpc_sender
448            .oneshot_request(RpcRequest::PooledUserCommands(query))
449            .await
450            .ok_or(Error::StateMachineEmptyResponse)?;
451
452        Ok(res
453            .into_iter()
454            .map(GraphQLUserCommands::try_from)
455            .collect::<Result<Vec<_>, _>>()?)
456    }
457
458    /// Retrieve all the scheduled zkApp commands for a specified sender that
459    ///  the current daemon sees in its transaction pool. All scheduled
460    ///  commands are queried if no sender is specified
461    ///
462    /// Arguments:
463    /// - `public_key`: base58 encoded [`AccountPublicKey`]
464    /// - `hashes`: list of base58 encoded [`TransactionHash`]es
465    /// - `ids`: list of base64 encoded [`MinaBaseZkappCommandTStableV1WireStableV1`]
466    async fn pooled_zkapp_commands(
467        public_key: Option<String>,
468        hashes: Option<Vec<String>>,
469        ids: Option<Vec<String>>,
470        context: &Context,
471    ) -> juniper::FieldResult<Vec<GraphQLZkapp>> {
472        let query = parse_pooled_commands_query(
473            public_key,
474            hashes,
475            ids,
476            MinaBaseZkappCommandTStableV1WireStableV1::from_base64,
477        )?;
478
479        let res: RpcPooledZkappCommandsResponse = context
480            .rpc_sender
481            .oneshot_request(RpcRequest::PooledZkappCommands(query))
482            .await
483            .ok_or(Error::StateMachineEmptyResponse)?;
484
485        Ok(res
486            .into_iter()
487            .map(GraphQLZkapp::try_from)
488            .collect::<Result<Vec<_>, _>>()?)
489    }
490
491    async fn genesis_block(context: &Context) -> juniper::FieldResult<GraphQLBlock> {
492        let block = context
493            .rpc_sender
494            .oneshot_request::<RpcGenesisBlockResponse>(RpcRequest::GenesisBlockGet)
495            .await
496            .ok_or(Error::StateMachineEmptyResponse)?
497            .ok_or(Error::StateMachineEmptyResponse)?;
498
499        Ok(GraphQLBlock::try_from(AppliedBlock {
500            block,
501            just_emitted_a_proof: false,
502        })?)
503    }
504
505    async fn snark_pool(context: &Context) -> juniper::FieldResult<Vec<GraphQLSnarkJob>> {
506        let jobs: RpcSnarkPoolCompletedJobsResponse = context
507            .rpc_sender
508            .oneshot_request(RpcRequest::SnarkPoolCompletedJobsGet)
509            .await
510            .ok_or(Error::StateMachineEmptyResponse)?;
511
512        Ok(jobs.iter().map(GraphQLSnarkJob::from).collect())
513    }
514
515    async fn pending_snark_work(
516        context: &Context,
517    ) -> juniper::FieldResult<Vec<GraphQLPendingSnarkWork>> {
518        let jobs: RpcSnarkPoolPendingJobsGetResponse = context
519            .rpc_sender
520            .oneshot_request(RpcRequest::SnarkPoolPendingJobsGet)
521            .await
522            .ok_or(Error::StateMachineEmptyResponse)?;
523
524        Ok(jobs
525            .into_iter()
526            .map(GraphQLPendingSnarkWork::try_from)
527            .collect::<Result<Vec<_>, _>>()?)
528    }
529
530    /// The chain-agnostic identifier of the network
531    #[graphql(name = "networkID")]
532    async fn network_id(_context: &Context) -> juniper::FieldResult<String> {
533        let res = format!("mina:{}", NetworkConfig::global().name);
534        Ok(res)
535    }
536
537    /// The version of the node (git commit hash)
538    async fn version(_context: &Context) -> juniper::FieldResult<String> {
539        let res = BuildEnv::get().git.commit_hash;
540        Ok(res)
541    }
542
543    async fn current_snark_worker(
544        &self,
545        context: &Context,
546    ) -> juniper::FieldResult<Option<GraphQLSnarkWorker>> {
547        let config: Option<RpcSnarkerConfig> = context
548            .rpc_sender
549            .oneshot_request(RpcRequest::SnarkerConfig)
550            .await
551            .ok_or(Error::StateMachineEmptyResponse)?;
552
553        let Some(config) = config else {
554            return Ok(None);
555        };
556
557        let account = context
558            .load_account(AccountId {
559                public_key: CompressedPubKey::try_from(&config.public_key)?,
560                token_id: TokenIdKeyHash::default().into(),
561            })
562            .await;
563
564        Ok(Some(GraphQLSnarkWorker {
565            key: config.public_key.to_string(),
566            account,
567            fee: config.fee.to_string(),
568        }))
569    }
570}
571
572async fn inject_tx<R>(
573    cmd: MinaBaseUserCommandStableV2,
574    context: &Context,
575) -> juniper::FieldResult<R>
576where
577    R: TryFrom<MinaBaseUserCommandStableV2>,
578{
579    let res: RpcTransactionInjectResponse = context
580        .rpc_sender
581        .oneshot_request(RpcRequest::TransactionInject(vec![cmd]))
582        .await
583        .ok_or(Error::StateMachineEmptyResponse)?;
584
585    match res {
586        RpcTransactionInjectResponse::Success(res) => {
587            let cmd: MinaBaseUserCommandStableV2 = match res.first().cloned() {
588                Some(cmd) => cmd.into(),
589                _ => unreachable!(),
590            };
591            cmd.try_into().map_err(|_| {
592                FieldError::new(
593                    "Failed to convert transaction to the required type".to_string(),
594                    graphql_value!(null),
595                )
596            })
597        }
598        RpcTransactionInjectResponse::Rejected(rejected) => {
599            let error_list = rejected
600                .into_iter()
601                .map(|(_, err)| graphql_value!({ "message": err.to_string() }))
602                .collect::<Vec<_>>();
603
604            Err(FieldError::new(
605                "Transaction rejected",
606                graphql_value!(juniper::Value::List(error_list)),
607            ))
608        }
609        RpcTransactionInjectResponse::Failure(failure) => {
610            let error_list = failure
611                .into_iter()
612                .map(|err| graphql_value!({ "message": err.to_string() }))
613                .collect::<Vec<_>>();
614
615            Err(FieldError::new(
616                "Transaction failed",
617                graphql_value!(juniper::Value::List(error_list)),
618            ))
619        }
620    }
621}
622
623#[derive(Clone, Debug)]
624struct Mutation;
625
626#[juniper::graphql_object(context = Context)]
627impl Mutation {
628    async fn send_zkapp(
629        input: zkapp::SendZkappInput,
630        context: &Context,
631    ) -> juniper::FieldResult<zkapp::GraphQLSendZkappResponse> {
632        inject_tx(input.try_into()?, context).await
633    }
634
635    async fn send_payment(
636        input: user_command::InputGraphQLPayment,
637        signature: user_command::UserCommandSignature,
638        context: &Context,
639    ) -> juniper::FieldResult<user_command::GraphQLSendPaymentResponse> {
640        // Grab the sender's account to get the infered nonce
641        let token_id = TokenIdKeyHash::default();
642        let public_key = AccountPublicKey::from_str(&input.from)
643            .map_err(|e| Error::Conversion(ConversionError::Base58Check(e)))?;
644
645        let accounts: Vec<Account> = context
646            .rpc_sender
647            .oneshot_request(RpcRequest::LedgerAccountsGet(
648                AccountQuery::PubKeyWithTokenId(public_key, token_id),
649            ))
650            .await
651            .ok_or(Error::StateMachineEmptyResponse)?;
652
653        let infered_nonce = accounts
654            .first()
655            .ok_or(Error::StateMachineEmptyResponse)?
656            .nonce;
657
658        let command = input
659            .create_user_command(infered_nonce, signature)
660            .map_err(Error::Conversion)?;
661
662        inject_tx(command, context).await
663    }
664
665    async fn send_delegation(
666        input: user_command::InputGraphQLDelegation,
667        signature: user_command::UserCommandSignature,
668        context: &Context,
669    ) -> juniper::FieldResult<user_command::GraphQLSendDelegationResponse> {
670        // Payment commands are always for the default (MINA) token
671        let token_id = TokenIdKeyHash::default();
672        let public_key = AccountPublicKey::from_str(&input.from)?;
673
674        // Grab the sender's account to get the infered nonce
675        let accounts: Vec<Account> = context
676            .rpc_sender
677            .oneshot_request(RpcRequest::LedgerAccountsGet(
678                AccountQuery::PubKeyWithTokenId(public_key, token_id),
679            ))
680            .await
681            .ok_or(Error::StateMachineEmptyResponse)?;
682
683        let infered_nonce = accounts
684            .first()
685            .ok_or(Error::StateMachineEmptyResponse)?
686            .nonce;
687        let command = input.create_user_command(infered_nonce, signature)?;
688
689        inject_tx(command, context).await
690    }
691}
692
693pub fn routes(
694    rpc_sernder: RpcSender,
695) -> impl Filter<Error = Rejection, Extract = impl Reply> + Clone {
696    let state = warp::any().map(move || Context::new(rpc_sernder.clone()));
697    let schema = RootNode::new(Query, Mutation, EmptySubscription::<Context>::new());
698    let graphql_filter = juniper_warp::make_graphql_filter(schema, state.boxed());
699    let graphiql_filter = juniper_warp::graphiql_filter("/graphql", None);
700    let playground_filter = juniper_warp::playground_filter("/graphql", None);
701
702    (warp::post().and(warp::path("graphql")).and(graphql_filter))
703        .or(warp::get()
704            .and(warp::path("playground"))
705            .and(playground_filter))
706        .or(warp::get().and(warp::path("graphiql")).and(graphiql_filter))
707
708    // warp::get()
709    //     .and(warp::path("graphiql"))
710    //     .and(juniper_warp::graphiql_filter("/graphql", None))
711    //     .or(warp::path("graphql").and(graphql_filter))
712}
713
714// let routes = (warp::post()
715//         .and(warp::path("graphql"))
716//         .and(juniper_warp::make_graphql_filter(
717//             schema.clone(),
718//             warp::any().map(|| Context),
719//         )))
720//     .or(
721//         warp::path("subscriptions").and(juniper_warp::subscriptions::make_ws_filter(
722//             schema,
723//             ConnectionConfig::new(Context),
724//         )),
725//     )
726//     .or(warp::get()
727//         .and(warp::path("playground"))
728//         .and(juniper_warp::playground_filter(
729//             "/graphql",
730//             Some("/subscriptions"),
731//         )))
732//     .or(warp::get()
733//         .and(warp::path("graphiql"))
734//         .and(juniper_warp::graphiql_filter(
735//             "/graphql",
736//             Some("/subscriptions"),
737//         )))
738//     .or(homepage)
739//     .with(log);
740
741/// Helper function used by [`Query::pooled_user_commands`] and [`Query::pooled_zkapp_commands`] to parse public key, transaction hashes and command ids
742fn parse_pooled_commands_query<ID, F>(
743    public_key: Option<String>,
744    hashes: Option<Vec<String>>,
745    ids: Option<Vec<String>>,
746    id_map_fn: F,
747) -> Result<PooledCommandsQuery<ID>, ConversionError>
748where
749    F: Fn(&str) -> Result<ID, conv::Error>,
750{
751    let public_key = match public_key {
752        Some(public_key) => Some(AccountPublicKey::from_str(&public_key)?),
753        None => None,
754    };
755
756    let hashes = match hashes {
757        Some(hashes) => Some(
758            hashes
759                .into_iter()
760                .map(|tx| TransactionHash::from_str(tx.as_str()))
761                .collect::<Result<Vec<_>, _>>()?,
762        ),
763        None => None,
764    };
765
766    let ids = match ids {
767        Some(ids) => Some(
768            ids.into_iter()
769                .map(|id| id_map_fn(id.as_str()))
770                .collect::<Result<Vec<_>, _>>()?,
771        ),
772        None => None,
773    };
774
775    Ok(PooledCommandsQuery {
776        public_key,
777        hashes,
778        ids,
779    })
780}