mina_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_core::{
6    block::AppliedBlock, consensus::ConsensusConstants, constants::constraint_constants,
7    NetworkConfig,
8};
9use mina_node_common::rpc::RpcSender;
10use mina_p2p_messages::v2::{
11    conv, LedgerHash, MinaBaseSignedCommandStableV2, MinaBaseUserCommandStableV2,
12    MinaBaseZkappCommandTStableV1WireStableV1, TokenIdKeyHash, TransactionHash,
13};
14use mina_signer::CompressedPubKey;
15use node::{
16    account::AccountPublicKey,
17    ledger::read::LedgerStatus,
18    rpc::{
19        AccountQuery, GetBlockQuery, PooledCommandsQuery, RpcBestChainResponse,
20        RpcGenesisBlockResponse, RpcGetBlockResponse, RpcLedgerAccountDelegatorsGetResponse,
21        RpcLedgerStatusGetResponse, RpcNodeStatus, RpcPooledUserCommandsResponse,
22        RpcPooledZkappCommandsResponse, RpcRequest, RpcSnarkPoolCompletedJobsResponse,
23        RpcSnarkPoolPendingJobsGetResponse, RpcSnarkerConfig, RpcStatusGetResponse,
24        RpcSyncStatsGetResponse, RpcTransactionInjectResponse, RpcTransactionStatusGetResponse,
25        SyncStatsQuery,
26    },
27    stats::sync::SyncKind,
28    BuildEnv,
29};
30use o1_utils::field_helpers::FieldHelpersError;
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 i.e. for
108/// one request which is the goal as we can have multiple sources for one
109/// request.
110/// This optimizes the number of request to the state machine
111pub struct Context {
112    rpc_sender: RpcSender,
113    account_loader: AccountLoader,
114    // Caches
115    statemachine_status_cache: OnceCell<Option<RpcNodeStatus>>,
116    best_tip_cache: OnceCell<Option<AppliedBlock>>,
117    ledger_status_cache: OnceCell<Option<LedgerStatus>>,
118}
119
120impl juniper::Context for Context {}
121
122impl Context {
123    pub fn new(rpc_sender: RpcSender) -> Self {
124        Self {
125            rpc_sender: rpc_sender.clone(),
126            statemachine_status_cache: OnceCell::new(),
127            best_tip_cache: OnceCell::new(),
128            ledger_status_cache: OnceCell::new(),
129            account_loader: create_account_loader(rpc_sender.clone()),
130        }
131    }
132
133    pub(crate) async fn get_or_fetch_status(&self) -> RpcStatusGetResponse {
134        self.statemachine_status_cache
135            .get_or_init(|| async {
136                self.rpc_sender
137                    .oneshot_request(RpcRequest::StatusGet)
138                    .await
139                    .flatten()
140            })
141            .await
142            .clone()
143    }
144
145    pub(crate) async fn get_or_fetch_best_tip(&self) -> Option<AppliedBlock> {
146        self.best_tip_cache
147            .get_or_init(|| async {
148                self.rpc_sender
149                    .oneshot_request(RpcRequest::BestChain(1))
150                    .await
151                    .and_then(|blocks: RpcBestChainResponse| blocks.first().cloned())
152            })
153            .await
154            .clone()
155    }
156
157    pub(crate) async fn get_or_fetch_ledger_status(
158        &self,
159        ledger_hash: &LedgerHash,
160    ) -> RpcLedgerStatusGetResponse {
161        self.ledger_status_cache
162            .get_or_init(|| async {
163                self.rpc_sender
164                    .oneshot_request(RpcRequest::LedgerStatusGet(ledger_hash.clone()))
165                    .await
166                    .flatten()
167            })
168            .await
169            .clone()
170    }
171
172    pub(crate) async fn load_account(&self, account_id: AccountId) -> Option<GraphQLAccount> {
173        self.account_loader.try_load(account_id).await.ok()?.ok()
174    }
175
176    pub async fn fetch_delegators(
177        &self,
178        ledger_hash: LedgerHash,
179        account_id: AccountId,
180    ) -> RpcLedgerAccountDelegatorsGetResponse {
181        self.rpc_sender
182            .oneshot_request(RpcRequest::LedgerAccountDelegatorsGet(
183                ledger_hash.clone(),
184                account_id.clone(),
185            ))
186            .await
187            .flatten()
188    }
189}
190
191#[derive(Clone, Copy, Debug, GraphQLEnum)]
192#[allow(clippy::upper_case_acronyms)]
193enum SyncStatus {
194    CONNECTING,
195    LISTENING,
196    OFFLINE,
197    BOOTSTRAP,
198    SYNCED,
199    CATCHUP,
200}
201
202#[derive(Clone, Copy, Debug)]
203pub struct Query;
204
205/// GraphQL Query endpoints exposed by the Mina node
206///
207/// # Available Queries:
208///
209/// ## Account Management
210/// - `account` - Retrieve account information for a public key
211/// - `current_snark_worker` - Get information about the current SNARK worker
212///
213/// ## Blockchain State
214/// - `sync_status` - Get the synchronization status of the node
215/// - `best_chain` - Retrieve the best chain of blocks
216/// - `block` - Get a specific block by hash or height
217/// - `genesis_block` - Retrieve the genesis block
218/// - `genesis_constants` - Get genesis configuration constants
219/// - `daemon_status` - Get the daemon status information
220///
221/// ## Transaction Pool
222/// - `pooled_user_commands` - Query pending user commands in the transaction
223///   pool
224/// - `pooled_zkapp_commands` - Query pending zkApp commands in the transaction
225///   pool
226/// - `transaction_status` - Check the status of a transaction
227///
228/// ## SNARK Pool
229/// - `snark_pool` - Get completed SNARK jobs
230/// - `pending_snark_work` - Get pending SNARK work items
231///
232/// ## Network Information
233/// - `network_id` - Get the chain-agnostic network identifier
234/// - `version` - Get the node version (git commit hash)
235#[juniper::graphql_object(context = Context)]
236impl Query {
237    /// Retrieve account information for a given public key
238    ///
239    /// # Arguments
240    /// - `public_key`: Base58-encoded public key
241    /// - `token`: Optional token ID (defaults to MINA token if not provided)
242    ///
243    /// # Returns
244    /// Account information including balance, nonce, delegate, and other fields
245    async fn account(
246        public_key: String,
247        token: Option<String>,
248        context: &Context,
249    ) -> juniper::FieldResult<account::GraphQLAccount> {
250        let public_key = AccountPublicKey::from_str(&public_key)?;
251        let req = match token {
252            None => AccountQuery::SinglePublicKey(public_key),
253            Some(token) => {
254                let token_id = TokenIdKeyHash::from_str(&token)?;
255                AccountQuery::PubKeyWithTokenId(public_key, token_id)
256            }
257        };
258        let accounts: Vec<Account> = context
259            .rpc_sender
260            .oneshot_request(RpcRequest::LedgerAccountsGet(req))
261            .await
262            .ok_or(Error::StateMachineEmptyResponse)?;
263
264        Ok(accounts
265            .first()
266            .cloned()
267            .ok_or(Error::StateMachineEmptyResponse)?
268            .try_into()?)
269    }
270
271    /// Get the current synchronization status of the node
272    ///
273    /// # Returns
274    /// One of: CONNECTING, LISTENING, OFFLINE, BOOTSTRAP, SYNCED, CATCHUP
275    async fn sync_status(context: &Context) -> juniper::FieldResult<SyncStatus> {
276        let state: RpcSyncStatsGetResponse = context
277            .rpc_sender
278            .oneshot_request(RpcRequest::SyncStatsGet(SyncStatsQuery { limit: Some(1) }))
279            .await
280            .ok_or(Error::StateMachineEmptyResponse)?;
281
282        if let Some(state) = state.as_ref().and_then(|s| s.first()) {
283            if state.synced.is_some() {
284                Ok(SyncStatus::SYNCED)
285            } else {
286                match &state.kind {
287                    SyncKind::Bootstrap => Ok(SyncStatus::BOOTSTRAP),
288                    SyncKind::Catchup => Ok(SyncStatus::CATCHUP),
289                }
290            }
291        } else {
292            Ok(SyncStatus::LISTENING)
293        }
294    }
295
296    /// Retrieve the best chain of blocks from the transition frontier
297    ///
298    /// # Arguments
299    /// - `max_length`: Maximum number of blocks to return
300    ///
301    /// # Returns
302    /// List of blocks in the best chain, ordered from newest to oldest
303    async fn best_chain(
304        max_length: i32,
305        context: &Context,
306    ) -> juniper::FieldResult<Vec<GraphQLBlock>> {
307        let best_chain: Vec<AppliedBlock> = context
308            .rpc_sender
309            .oneshot_request(RpcRequest::BestChain(max_length as u32))
310            .await
311            .ok_or(Error::StateMachineEmptyResponse)?;
312
313        Ok(best_chain
314            .into_iter()
315            .map(|v| v.try_into())
316            .collect::<Result<Vec<_>, _>>()?)
317    }
318
319    /// Get daemon status information including chain ID and configuration
320    ///
321    /// # Returns
322    /// Daemon status with chain ID, configuration, and other metadata
323    async fn daemon_status(
324        _context: &Context,
325    ) -> juniper::FieldResult<constants::GraphQLDaemonStatus> {
326        Ok(constants::GraphQLDaemonStatus)
327    }
328
329    /// Retrieve genesis configuration constants
330    ///
331    /// # Returns
332    /// Genesis constants including protocol parameters and constraints
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    /// Check the status of a transaction
350    ///
351    /// # Arguments
352    /// - `payment`: Base64-encoded signed command (mutually exclusive with
353    ///   zkapp_transaction)
354    /// - `zkapp_transaction`: Base64-encoded zkApp command (mutually exclusive with payment)
355    ///
356    /// # Returns
357    /// Transaction status (PENDING, INCLUDED, or UNKNOWN)
358    async fn transaction_status(
359        payment: Option<String>,
360        zkapp_transaction: Option<String>,
361        context: &Context,
362    ) -> juniper::FieldResult<GraphQLTransactionStatus> {
363        if payment.is_some() && zkapp_transaction.is_some() {
364            return Err(Error::Custom(
365                "Cannot provide both payment and zkapp transaction".to_string(),
366            )
367            .into());
368        }
369
370        let tx = if let Some(payment) = payment {
371            MinaBaseUserCommandStableV2::SignedCommand(MinaBaseSignedCommandStableV2::from_base64(
372                &payment,
373            )?)
374        } else if let Some(zkapp_transaction) = zkapp_transaction {
375            MinaBaseUserCommandStableV2::ZkappCommand(
376                MinaBaseZkappCommandTStableV1WireStableV1::from_base64(&zkapp_transaction)?,
377            )
378        } else {
379            return Err(Error::Custom(
380                "Must provide either payment or zkapp transaction".to_string(),
381            )
382            .into());
383        };
384        let res: RpcTransactionStatusGetResponse = context
385            .rpc_sender
386            .oneshot_request(RpcRequest::TransactionStatusGet(tx))
387            .await
388            .ok_or(Error::StateMachineEmptyResponse)?;
389
390        Ok(GraphQLTransactionStatus::from(res))
391    }
392
393    /// Retrieve a block with the given state hash or height from the transition frontier
394    ///
395    /// # Arguments
396    /// - `height`: Block height (mutually exclusive with state_hash)
397    /// - `state_hash`: Block state hash (mutually exclusive with height)
398    ///
399    /// # Returns
400    /// Block data including transactions, SNARK jobs, and metadata
401    async fn block(
402        height: Option<i32>,
403        state_hash: Option<String>,
404        context: &Context,
405    ) -> juniper::FieldResult<GraphQLBlock> {
406        let query = match (height, state_hash) {
407            (Some(height), None) => GetBlockQuery::Height(height.try_into().unwrap_or(u32::MAX)),
408            (None, Some(state_hash)) => GetBlockQuery::Hash(state_hash.parse()?),
409            _ => {
410                return Err(Error::Custom(
411                    "Must provide exactly one of state hash, height".to_owned(),
412                )
413                .into());
414            }
415        };
416
417        let res: Option<RpcGetBlockResponse> = context
418            .rpc_sender
419            .oneshot_request(RpcRequest::GetBlock(query.clone()))
420            .await;
421
422        match res {
423            None => Err(Error::Custom("response channel dropped".to_owned()).into()),
424            Some(None) => match query {
425                GetBlockQuery::Hash(hash) => Err(Error::Custom(format!(
426                    "Could not find block with hash: `{}` in transition frontier",
427                    hash
428                ))
429                .into()),
430                GetBlockQuery::Height(height) => Err(Error::Custom(format!(
431                    "Could not find block with height: `{}` in transition frontier",
432                    height
433                ))
434                .into()),
435            },
436            Some(Some(block)) => Ok(GraphQLBlock::try_from(block)?),
437        }
438    }
439
440    /// Retrieve all the scheduled user commands for a specified sender that
441    /// the current daemon sees in its transaction pool. All scheduled
442    /// commands are queried if no sender is specified
443    ///
444    /// Arguments:
445    /// - `public_key`: base58 encoded [`AccountPublicKey`]
446    /// - `hashes`: list of base58 encoded [`TransactionHash`]es
447    /// - `ids`: list of base64 encoded [`MinaBaseZkappCommandTStableV1WireStableV1`]
448    async fn pooled_user_commands(
449        &self,
450        public_key: Option<String>,
451        hashes: Option<Vec<String>>,
452        ids: Option<Vec<String>>,
453        context: &Context,
454    ) -> juniper::FieldResult<Vec<GraphQLUserCommands>> {
455        let query = parse_pooled_commands_query(
456            public_key,
457            hashes,
458            ids,
459            MinaBaseSignedCommandStableV2::from_base64,
460        )?;
461
462        let res: RpcPooledUserCommandsResponse = context
463            .rpc_sender
464            .oneshot_request(RpcRequest::PooledUserCommands(query))
465            .await
466            .ok_or(Error::StateMachineEmptyResponse)?;
467
468        Ok(res
469            .into_iter()
470            .map(GraphQLUserCommands::try_from)
471            .collect::<Result<Vec<_>, _>>()?)
472    }
473
474    /// Retrieve all the scheduled zkApp commands for a specified sender that
475    ///  the current daemon sees in its transaction pool. All scheduled
476    ///  commands are queried if no sender is specified
477    ///
478    /// Arguments:
479    /// - `public_key`: base58 encoded [`AccountPublicKey`]
480    /// - `hashes`: list of base58 encoded [`TransactionHash`]es
481    /// - `ids`: list of base64 encoded [`MinaBaseZkappCommandTStableV1WireStableV1`]
482    async fn pooled_zkapp_commands(
483        public_key: Option<String>,
484        hashes: Option<Vec<String>>,
485        ids: Option<Vec<String>>,
486        context: &Context,
487    ) -> juniper::FieldResult<Vec<GraphQLZkapp>> {
488        let query = parse_pooled_commands_query(
489            public_key,
490            hashes,
491            ids,
492            MinaBaseZkappCommandTStableV1WireStableV1::from_base64,
493        )?;
494
495        let res: RpcPooledZkappCommandsResponse = context
496            .rpc_sender
497            .oneshot_request(RpcRequest::PooledZkappCommands(query))
498            .await
499            .ok_or(Error::StateMachineEmptyResponse)?;
500
501        Ok(res
502            .into_iter()
503            .map(GraphQLZkapp::try_from)
504            .collect::<Result<Vec<_>, _>>()?)
505    }
506
507    /// Retrieve the genesis block
508    ///
509    /// # Returns
510    /// The genesis block data
511    async fn genesis_block(context: &Context) -> juniper::FieldResult<GraphQLBlock> {
512        let block = context
513            .rpc_sender
514            .oneshot_request::<RpcGenesisBlockResponse>(RpcRequest::GenesisBlockGet)
515            .await
516            .ok_or(Error::StateMachineEmptyResponse)?
517            .ok_or(Error::StateMachineEmptyResponse)?;
518
519        Ok(GraphQLBlock::try_from(AppliedBlock {
520            block,
521            just_emitted_a_proof: false,
522        })?)
523    }
524
525    /// Get completed SNARK jobs from the SNARK pool
526    ///
527    /// # Returns
528    /// List of completed SNARK jobs with proofs and fees
529    async fn snark_pool(context: &Context) -> juniper::FieldResult<Vec<GraphQLSnarkJob>> {
530        let jobs: RpcSnarkPoolCompletedJobsResponse = context
531            .rpc_sender
532            .oneshot_request(RpcRequest::SnarkPoolCompletedJobsGet)
533            .await
534            .ok_or(Error::StateMachineEmptyResponse)?;
535
536        Ok(jobs.iter().map(GraphQLSnarkJob::from).collect())
537    }
538
539    /// Get pending SNARK work items that need to be completed
540    ///
541    /// # Returns
542    /// List of pending SNARK work items with job specifications
543    async fn pending_snark_work(
544        context: &Context,
545    ) -> juniper::FieldResult<Vec<GraphQLPendingSnarkWork>> {
546        let jobs: RpcSnarkPoolPendingJobsGetResponse = context
547            .rpc_sender
548            .oneshot_request(RpcRequest::SnarkPoolPendingJobsGet)
549            .await
550            .ok_or(Error::StateMachineEmptyResponse)?;
551
552        Ok(jobs
553            .into_iter()
554            .map(GraphQLPendingSnarkWork::try_from)
555            .collect::<Result<Vec<_>, _>>()?)
556    }
557
558    /// The chain-agnostic identifier of the network
559    ///
560    /// # Returns
561    /// Network identifier in the format "mina:<network_name>"
562    #[graphql(name = "networkID")]
563    async fn network_id(_context: &Context) -> juniper::FieldResult<String> {
564        let res = format!("mina:{}", NetworkConfig::global().name);
565        Ok(res)
566    }
567
568    /// The version of the node (git commit hash)
569    ///
570    /// # Returns
571    /// Git commit hash of the current node build
572    async fn version(_context: &Context) -> juniper::FieldResult<String> {
573        let res = BuildEnv::get().git.commit_hash;
574        Ok(res)
575    }
576
577    /// Get information about the current SNARK worker if configured
578    ///
579    /// # Returns
580    /// SNARK worker configuration including public key, account, and fee
581    async fn current_snark_worker(
582        &self,
583        context: &Context,
584    ) -> juniper::FieldResult<Option<GraphQLSnarkWorker>> {
585        let config: Option<RpcSnarkerConfig> = context
586            .rpc_sender
587            .oneshot_request(RpcRequest::SnarkerConfig)
588            .await
589            .ok_or(Error::StateMachineEmptyResponse)?;
590
591        let Some(config) = config else {
592            return Ok(None);
593        };
594
595        let account = context
596            .load_account(AccountId {
597                public_key: CompressedPubKey::try_from(&config.public_key)?,
598                token_id: TokenIdKeyHash::default().into(),
599            })
600            .await;
601
602        Ok(Some(GraphQLSnarkWorker {
603            key: config.public_key.to_string(),
604            account,
605            fee: config.fee.to_string(),
606        }))
607    }
608}
609
610async fn inject_tx<R>(
611    cmd: MinaBaseUserCommandStableV2,
612    context: &Context,
613) -> juniper::FieldResult<R>
614where
615    R: TryFrom<MinaBaseUserCommandStableV2>,
616{
617    let res: RpcTransactionInjectResponse = context
618        .rpc_sender
619        .oneshot_request(RpcRequest::TransactionInject(vec![cmd]))
620        .await
621        .ok_or(Error::StateMachineEmptyResponse)?;
622
623    match res {
624        RpcTransactionInjectResponse::Success(res) => {
625            let cmd: MinaBaseUserCommandStableV2 = match res.first().cloned() {
626                Some(cmd) => cmd.into(),
627                _ => unreachable!(),
628            };
629            cmd.try_into().map_err(|_| {
630                FieldError::new(
631                    "Failed to convert transaction to the required type".to_string(),
632                    graphql_value!(null),
633                )
634            })
635        }
636        RpcTransactionInjectResponse::Rejected(rejected) => {
637            let error_list = rejected
638                .into_iter()
639                .map(|(_, err)| graphql_value!({ "message": err.to_string() }))
640                .collect::<Vec<_>>();
641
642            Err(FieldError::new(
643                "Transaction rejected",
644                graphql_value!(juniper::Value::List(error_list)),
645            ))
646        }
647        RpcTransactionInjectResponse::Failure(failure) => {
648            let error_list = failure
649                .into_iter()
650                .map(|err| graphql_value!({ "message": err.to_string() }))
651                .collect::<Vec<_>>();
652
653            Err(FieldError::new(
654                "Transaction failed",
655                graphql_value!(juniper::Value::List(error_list)),
656            ))
657        }
658    }
659}
660
661#[derive(Clone, Debug)]
662pub struct Mutation;
663
664/// GraphQL Mutation endpoints exposed by the Mina node
665///
666/// # Available Mutations:
667///
668/// ## Transaction Submission
669/// - `send_zkapp` - Submit a zkApp transaction to the network
670/// - `send_payment` - Send a payment transaction
671/// - `send_delegation` - Send a delegation transaction
672#[juniper::graphql_object(context = Context)]
673impl Mutation {
674    /// Submit a zkApp transaction to the network
675    ///
676    /// # Arguments
677    /// - `input`: zkApp command with account updates and fee payer information
678    ///
679    /// # Returns
680    /// Transaction response with hash and zkApp command details
681    async fn send_zkapp(
682        input: zkapp::SendZkappInput,
683        context: &Context,
684    ) -> juniper::FieldResult<zkapp::GraphQLSendZkappResponse> {
685        inject_tx(input.try_into()?, context).await
686    }
687
688    /// Send a payment transaction
689    ///
690    /// # Arguments
691    /// - `input`: Payment details including sender, receiver, amount, fee, and memo
692    /// - `signature`: Transaction signature
693    ///
694    /// # Returns
695    /// Payment response with transaction hash and details
696    async fn send_payment(
697        input: user_command::InputGraphQLPayment,
698        signature: user_command::UserCommandSignature,
699        context: &Context,
700    ) -> juniper::FieldResult<user_command::GraphQLSendPaymentResponse> {
701        // Grab the sender's account to get the infered nonce
702        let token_id = TokenIdKeyHash::default();
703        let public_key = AccountPublicKey::from_str(&input.from)
704            .map_err(|e| Error::Conversion(ConversionError::Base58Check(e)))?;
705
706        let accounts: Vec<Account> = context
707            .rpc_sender
708            .oneshot_request(RpcRequest::LedgerAccountsGet(
709                AccountQuery::PubKeyWithTokenId(public_key, token_id),
710            ))
711            .await
712            .ok_or(Error::StateMachineEmptyResponse)?;
713
714        let infered_nonce = accounts
715            .first()
716            .ok_or(Error::StateMachineEmptyResponse)?
717            .nonce;
718
719        let command = input
720            .create_user_command(infered_nonce, signature)
721            .map_err(Error::Conversion)?;
722
723        inject_tx(command, context).await
724    }
725
726    /// Send a delegation transaction
727    ///
728    /// # Arguments
729    /// - `input`: Delegation details including delegator, delegate, fee, and memo
730    /// - `signature`: Transaction signature
731    ///
732    /// # Returns
733    /// Delegation response with transaction hash and details
734    async fn send_delegation(
735        input: user_command::InputGraphQLDelegation,
736        signature: user_command::UserCommandSignature,
737        context: &Context,
738    ) -> juniper::FieldResult<user_command::GraphQLSendDelegationResponse> {
739        // Payment commands are always for the default (MINA) token
740        let token_id = TokenIdKeyHash::default();
741        let public_key = AccountPublicKey::from_str(&input.from)?;
742
743        // Grab the sender's account to get the infered nonce
744        let accounts: Vec<Account> = context
745            .rpc_sender
746            .oneshot_request(RpcRequest::LedgerAccountsGet(
747                AccountQuery::PubKeyWithTokenId(public_key, token_id),
748            ))
749            .await
750            .ok_or(Error::StateMachineEmptyResponse)?;
751
752        let infered_nonce = accounts
753            .first()
754            .ok_or(Error::StateMachineEmptyResponse)?
755            .nonce;
756        let command = input.create_user_command(infered_nonce, signature)?;
757
758        inject_tx(command, context).await
759    }
760}
761
762pub fn routes(
763    rpc_sernder: RpcSender,
764) -> impl Filter<Error = Rejection, Extract = impl Reply> + Clone {
765    let state = warp::any().map(move || Context::new(rpc_sernder.clone()));
766    let schema = RootNode::new(Query, Mutation, EmptySubscription::<Context>::new());
767    let graphql_filter = juniper_warp::make_graphql_filter(schema, state.boxed());
768    let graphiql_filter = juniper_warp::graphiql_filter("/graphql", None);
769    let playground_filter = juniper_warp::playground_filter("/graphql", None);
770
771    (warp::post().and(warp::path("graphql")).and(graphql_filter))
772        .or(warp::get()
773            .and(warp::path("playground"))
774            .and(playground_filter))
775        .or(warp::get().and(warp::path("graphiql")).and(graphiql_filter))
776}
777
778/// Helper function used by [`Query::pooled_user_commands`] and
779/// [`Query::pooled_zkapp_commands`] to parse public key, transaction hashes and
780/// command ids
781fn parse_pooled_commands_query<ID, F>(
782    public_key: Option<String>,
783    hashes: Option<Vec<String>>,
784    ids: Option<Vec<String>>,
785    id_map_fn: F,
786) -> Result<PooledCommandsQuery<ID>, ConversionError>
787where
788    F: Fn(&str) -> Result<ID, conv::Error>,
789{
790    let public_key = match public_key {
791        Some(public_key) => Some(AccountPublicKey::from_str(&public_key)?),
792        None => None,
793    };
794
795    let hashes = match hashes {
796        Some(hashes) => Some(
797            hashes
798                .into_iter()
799                .map(|tx| TransactionHash::from_str(tx.as_str()))
800                .collect::<Result<Vec<_>, _>>()?,
801        ),
802        None => None,
803    };
804
805    let ids = match ids {
806        Some(ids) => Some(
807            ids.into_iter()
808                .map(|id| id_map_fn(id.as_str()))
809                .collect::<Result<Vec<_>, _>>()?,
810        ),
811        None => None,
812    };
813
814    Ok(PooledCommandsQuery {
815        public_key,
816        hashes,
817        ids,
818    })
819}