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, Debug)]
203struct ProtocolState {
204    consensus_state: ConsensusState,
205    blockchain_state: BlockchainState,
206}
207
208#[juniper::graphql_object(context = Context)]
209impl ProtocolState {
210    fn consensus_state(&self) -> &ConsensusState {
211        &self.consensus_state
212    }
213
214    fn blockchain_state(&self) -> &BlockchainState {
215        &self.blockchain_state
216    }
217}
218
219#[derive(Clone, Debug)]
220struct ConsensusState {
221    block_height: i32,
222}
223
224#[juniper::graphql_object(context = Context)]
225impl ConsensusState {
226    fn block_height(&self) -> i32 {
227        self.block_height
228    }
229}
230
231#[derive(Clone, Debug)]
232struct BlockchainState {
233    snarked_ledger_hash: String,
234}
235
236#[juniper::graphql_object(context = Context)]
237impl BlockchainState {
238    fn snarked_ledger_hash(&self) -> &str {
239        &self.snarked_ledger_hash
240    }
241}
242
243#[derive(Clone, Debug)]
244struct BestChain {
245    state_hash: String,
246    protocol_state: ProtocolState,
247}
248
249#[juniper::graphql_object(context = Context)]
250impl BestChain {
251    fn state_hash(&self) -> &str {
252        &self.state_hash
253    }
254
255    fn protocol_state(&self) -> &ProtocolState {
256        &self.protocol_state
257    }
258}
259
260#[derive(Clone, Copy, Debug)]
261pub struct Query;
262
263/// GraphQL Query endpoints exposed by the Mina node
264///
265/// # Available Queries:
266///
267/// ## Account Management
268/// - `account` - Retrieve account information for a public key
269/// - `current_snark_worker` - Get information about the current SNARK worker
270///
271/// ## Blockchain State
272/// - `sync_status` - Get the synchronization status of the node
273/// - `best_chain` - Retrieve the best chain of blocks
274/// - `block` - Get a specific block by hash or height
275/// - `genesis_block` - Retrieve the genesis block
276/// - `genesis_constants` - Get genesis configuration constants
277/// - `daemon_status` - Get the daemon status information
278///
279/// ## Transaction Pool
280/// - `pooled_user_commands` - Query pending user commands in the transaction
281///   pool
282/// - `pooled_zkapp_commands` - Query pending zkApp commands in the transaction
283///   pool
284/// - `transaction_status` - Check the status of a transaction
285///
286/// ## SNARK Pool
287/// - `snark_pool` - Get completed SNARK jobs
288/// - `pending_snark_work` - Get pending SNARK work items
289///
290/// ## Network Information
291/// - `network_id` - Get the chain-agnostic network identifier
292/// - `version` - Get the node version (git commit hash)
293#[juniper::graphql_object(context = Context)]
294impl Query {
295    /// Retrieve account information for a given public key
296    ///
297    /// # Arguments
298    /// - `public_key`: Base58-encoded public key
299    /// - `token`: Optional token ID (defaults to MINA token if not provided)
300    ///
301    /// # Returns
302    /// Account information including balance, nonce, delegate, and other fields
303    async fn account(
304        public_key: String,
305        token: Option<String>,
306        context: &Context,
307    ) -> juniper::FieldResult<account::GraphQLAccount> {
308        let public_key = AccountPublicKey::from_str(&public_key)?;
309        let req = match token {
310            None => AccountQuery::SinglePublicKey(public_key),
311            Some(token) => {
312                let token_id = TokenIdKeyHash::from_str(&token)?;
313                AccountQuery::PubKeyWithTokenId(public_key, token_id)
314            }
315        };
316        let accounts: Vec<Account> = context
317            .rpc_sender
318            .oneshot_request(RpcRequest::LedgerAccountsGet(req))
319            .await
320            .ok_or(Error::StateMachineEmptyResponse)?;
321
322        Ok(accounts
323            .first()
324            .cloned()
325            .ok_or(Error::StateMachineEmptyResponse)?
326            .try_into()?)
327    }
328
329    /// Get the current synchronization status of the node
330    ///
331    /// # Returns
332    /// One of: CONNECTING, LISTENING, OFFLINE, BOOTSTRAP, SYNCED, CATCHUP
333    async fn sync_status(context: &Context) -> juniper::FieldResult<SyncStatus> {
334        let state: RpcSyncStatsGetResponse = context
335            .rpc_sender
336            .oneshot_request(RpcRequest::SyncStatsGet(SyncStatsQuery { limit: Some(1) }))
337            .await
338            .ok_or(Error::StateMachineEmptyResponse)?;
339
340        if let Some(state) = state.as_ref().and_then(|s| s.first()) {
341            if state.synced.is_some() {
342                Ok(SyncStatus::SYNCED)
343            } else {
344                match &state.kind {
345                    SyncKind::Bootstrap => Ok(SyncStatus::BOOTSTRAP),
346                    SyncKind::Catchup => Ok(SyncStatus::CATCHUP),
347                }
348            }
349        } else {
350            Ok(SyncStatus::LISTENING)
351        }
352    }
353
354    /// Retrieve the best chain of blocks from the transition frontier
355    ///
356    /// # Arguments
357    /// - `max_length`: Maximum number of blocks to return
358    ///
359    /// # Returns
360    /// List of blocks in the best chain, ordered from newest to oldest
361    async fn best_chain(
362        max_length: i32,
363        context: &Context,
364    ) -> juniper::FieldResult<Vec<GraphQLBlock>> {
365        let best_chain: Vec<AppliedBlock> = context
366            .rpc_sender
367            .oneshot_request(RpcRequest::BestChain(max_length as u32))
368            .await
369            .ok_or(Error::StateMachineEmptyResponse)?;
370
371        Ok(best_chain
372            .into_iter()
373            .map(|v| v.try_into())
374            .collect::<Result<Vec<_>, _>>()?)
375    }
376
377    /// Get daemon status information including chain ID and configuration
378    ///
379    /// # Returns
380    /// Daemon status with chain ID, configuration, and other metadata
381    async fn daemon_status(
382        _context: &Context,
383    ) -> juniper::FieldResult<constants::GraphQLDaemonStatus> {
384        Ok(constants::GraphQLDaemonStatus)
385    }
386
387    /// Retrieve genesis configuration constants
388    ///
389    /// # Returns
390    /// Genesis constants including protocol parameters and constraints
391    async fn genesis_constants(
392        context: &Context,
393    ) -> juniper::FieldResult<constants::GraphQLGenesisConstants> {
394        let consensus_constants: ConsensusConstants = context
395            .rpc_sender
396            .oneshot_request(RpcRequest::ConsensusConstantsGet)
397            .await
398            .ok_or(Error::StateMachineEmptyResponse)?;
399        let constraint_constants = constraint_constants();
400
401        Ok(constants::GraphQLGenesisConstants::try_new(
402            constraint_constants.clone(),
403            consensus_constants,
404        )?)
405    }
406
407    /// Check the status of a transaction
408    ///
409    /// # Arguments
410    /// - `payment`: Base64-encoded signed command (mutually exclusive with
411    ///   zkapp_transaction)
412    /// - `zkapp_transaction`: Base64-encoded zkApp command (mutually exclusive with payment)
413    ///
414    /// # Returns
415    /// Transaction status (PENDING, INCLUDED, or UNKNOWN)
416    async fn transaction_status(
417        payment: Option<String>,
418        zkapp_transaction: Option<String>,
419        context: &Context,
420    ) -> juniper::FieldResult<GraphQLTransactionStatus> {
421        if payment.is_some() && zkapp_transaction.is_some() {
422            return Err(Error::Custom(
423                "Cannot provide both payment and zkapp transaction".to_string(),
424            )
425            .into());
426        }
427
428        let tx = if let Some(payment) = payment {
429            MinaBaseUserCommandStableV2::SignedCommand(MinaBaseSignedCommandStableV2::from_base64(
430                &payment,
431            )?)
432        } else if let Some(zkapp_transaction) = zkapp_transaction {
433            MinaBaseUserCommandStableV2::ZkappCommand(
434                MinaBaseZkappCommandTStableV1WireStableV1::from_base64(&zkapp_transaction)?,
435            )
436        } else {
437            return Err(Error::Custom(
438                "Must provide either payment or zkapp transaction".to_string(),
439            )
440            .into());
441        };
442        let res: RpcTransactionStatusGetResponse = context
443            .rpc_sender
444            .oneshot_request(RpcRequest::TransactionStatusGet(tx))
445            .await
446            .ok_or(Error::StateMachineEmptyResponse)?;
447
448        Ok(GraphQLTransactionStatus::from(res))
449    }
450
451    /// Retrieve a block with the given state hash or height from the transition frontier
452    ///
453    /// # Arguments
454    /// - `height`: Block height (mutually exclusive with state_hash)
455    /// - `state_hash`: Block state hash (mutually exclusive with height)
456    ///
457    /// # Returns
458    /// Block data including transactions, SNARK jobs, and metadata
459    async fn block(
460        height: Option<i32>,
461        state_hash: Option<String>,
462        context: &Context,
463    ) -> juniper::FieldResult<GraphQLBlock> {
464        let query = match (height, state_hash) {
465            (Some(height), None) => GetBlockQuery::Height(height.try_into().unwrap_or(u32::MAX)),
466            (None, Some(state_hash)) => GetBlockQuery::Hash(state_hash.parse()?),
467            _ => {
468                return Err(Error::Custom(
469                    "Must provide exactly one of state hash, height".to_owned(),
470                )
471                .into());
472            }
473        };
474
475        let res: Option<RpcGetBlockResponse> = context
476            .rpc_sender
477            .oneshot_request(RpcRequest::GetBlock(query.clone()))
478            .await;
479
480        match res {
481            None => Err(Error::Custom("response channel dropped".to_owned()).into()),
482            Some(None) => match query {
483                GetBlockQuery::Hash(hash) => Err(Error::Custom(format!(
484                    "Could not find block with hash: `{}` in transition frontier",
485                    hash
486                ))
487                .into()),
488                GetBlockQuery::Height(height) => Err(Error::Custom(format!(
489                    "Could not find block with height: `{}` in transition frontier",
490                    height
491                ))
492                .into()),
493            },
494            Some(Some(block)) => Ok(GraphQLBlock::try_from(block)?),
495        }
496    }
497
498    /// Retrieve all the scheduled user commands for a specified sender that
499    /// the current daemon sees in its transaction pool. All scheduled
500    /// commands are queried if no sender is specified
501    ///
502    /// Arguments:
503    /// - `public_key`: base58 encoded [`AccountPublicKey`]
504    /// - `hashes`: list of base58 encoded [`TransactionHash`]es
505    /// - `ids`: list of base64 encoded [`MinaBaseZkappCommandTStableV1WireStableV1`]
506    async fn pooled_user_commands(
507        &self,
508        public_key: Option<String>,
509        hashes: Option<Vec<String>>,
510        ids: Option<Vec<String>>,
511        context: &Context,
512    ) -> juniper::FieldResult<Vec<GraphQLUserCommands>> {
513        let query = parse_pooled_commands_query(
514            public_key,
515            hashes,
516            ids,
517            MinaBaseSignedCommandStableV2::from_base64,
518        )?;
519
520        let res: RpcPooledUserCommandsResponse = context
521            .rpc_sender
522            .oneshot_request(RpcRequest::PooledUserCommands(query))
523            .await
524            .ok_or(Error::StateMachineEmptyResponse)?;
525
526        Ok(res
527            .into_iter()
528            .map(GraphQLUserCommands::try_from)
529            .collect::<Result<Vec<_>, _>>()?)
530    }
531
532    /// Retrieve all the scheduled zkApp commands for a specified sender that
533    ///  the current daemon sees in its transaction pool. All scheduled
534    ///  commands are queried if no sender is specified
535    ///
536    /// Arguments:
537    /// - `public_key`: base58 encoded [`AccountPublicKey`]
538    /// - `hashes`: list of base58 encoded [`TransactionHash`]es
539    /// - `ids`: list of base64 encoded [`MinaBaseZkappCommandTStableV1WireStableV1`]
540    async fn pooled_zkapp_commands(
541        public_key: Option<String>,
542        hashes: Option<Vec<String>>,
543        ids: Option<Vec<String>>,
544        context: &Context,
545    ) -> juniper::FieldResult<Vec<GraphQLZkapp>> {
546        let query = parse_pooled_commands_query(
547            public_key,
548            hashes,
549            ids,
550            MinaBaseZkappCommandTStableV1WireStableV1::from_base64,
551        )?;
552
553        let res: RpcPooledZkappCommandsResponse = context
554            .rpc_sender
555            .oneshot_request(RpcRequest::PooledZkappCommands(query))
556            .await
557            .ok_or(Error::StateMachineEmptyResponse)?;
558
559        Ok(res
560            .into_iter()
561            .map(GraphQLZkapp::try_from)
562            .collect::<Result<Vec<_>, _>>()?)
563    }
564
565    /// Retrieve the genesis block
566    ///
567    /// # Returns
568    /// The genesis block data
569    async fn genesis_block(context: &Context) -> juniper::FieldResult<GraphQLBlock> {
570        let block = context
571            .rpc_sender
572            .oneshot_request::<RpcGenesisBlockResponse>(RpcRequest::GenesisBlockGet)
573            .await
574            .ok_or(Error::StateMachineEmptyResponse)?
575            .ok_or(Error::StateMachineEmptyResponse)?;
576
577        Ok(GraphQLBlock::try_from(AppliedBlock {
578            block,
579            just_emitted_a_proof: false,
580        })?)
581    }
582
583    /// Get completed SNARK jobs from the SNARK pool
584    ///
585    /// # Returns
586    /// List of completed SNARK jobs with proofs and fees
587    async fn snark_pool(context: &Context) -> juniper::FieldResult<Vec<GraphQLSnarkJob>> {
588        let jobs: RpcSnarkPoolCompletedJobsResponse = context
589            .rpc_sender
590            .oneshot_request(RpcRequest::SnarkPoolCompletedJobsGet)
591            .await
592            .ok_or(Error::StateMachineEmptyResponse)?;
593
594        Ok(jobs.iter().map(GraphQLSnarkJob::from).collect())
595    }
596
597    /// Get pending SNARK work items that need to be completed
598    ///
599    /// # Returns
600    /// List of pending SNARK work items with job specifications
601    async fn pending_snark_work(
602        context: &Context,
603    ) -> juniper::FieldResult<Vec<GraphQLPendingSnarkWork>> {
604        let jobs: RpcSnarkPoolPendingJobsGetResponse = context
605            .rpc_sender
606            .oneshot_request(RpcRequest::SnarkPoolPendingJobsGet)
607            .await
608            .ok_or(Error::StateMachineEmptyResponse)?;
609
610        Ok(jobs
611            .into_iter()
612            .map(GraphQLPendingSnarkWork::try_from)
613            .collect::<Result<Vec<_>, _>>()?)
614    }
615
616    /// The chain-agnostic identifier of the network
617    ///
618    /// # Returns
619    /// Network identifier in the format "mina:<network_name>"
620    #[graphql(name = "networkID")]
621    async fn network_id(_context: &Context) -> juniper::FieldResult<String> {
622        let res = format!("mina:{}", NetworkConfig::global().name);
623        Ok(res)
624    }
625
626    /// The version of the node (git commit hash)
627    ///
628    /// # Returns
629    /// Git commit hash of the current node build
630    async fn version(_context: &Context) -> juniper::FieldResult<String> {
631        let res = BuildEnv::get().git.commit_hash;
632        Ok(res)
633    }
634
635    /// Get information about the current SNARK worker if configured
636    ///
637    /// # Returns
638    /// SNARK worker configuration including public key, account, and fee
639    async fn current_snark_worker(
640        &self,
641        context: &Context,
642    ) -> juniper::FieldResult<Option<GraphQLSnarkWorker>> {
643        let config: Option<RpcSnarkerConfig> = context
644            .rpc_sender
645            .oneshot_request(RpcRequest::SnarkerConfig)
646            .await
647            .ok_or(Error::StateMachineEmptyResponse)?;
648
649        let Some(config) = config else {
650            return Ok(None);
651        };
652
653        let account = context
654            .load_account(AccountId {
655                public_key: CompressedPubKey::try_from(&config.public_key)?,
656                token_id: TokenIdKeyHash::default().into(),
657            })
658            .await;
659
660        Ok(Some(GraphQLSnarkWorker {
661            key: config.public_key.to_string(),
662            account,
663            fee: config.fee.to_string(),
664        }))
665    }
666}
667
668async fn inject_tx<R>(
669    cmd: MinaBaseUserCommandStableV2,
670    context: &Context,
671) -> juniper::FieldResult<R>
672where
673    R: TryFrom<MinaBaseUserCommandStableV2>,
674{
675    let res: RpcTransactionInjectResponse = context
676        .rpc_sender
677        .oneshot_request(RpcRequest::TransactionInject(vec![cmd]))
678        .await
679        .ok_or(Error::StateMachineEmptyResponse)?;
680
681    match res {
682        RpcTransactionInjectResponse::Success(res) => {
683            let cmd: MinaBaseUserCommandStableV2 = match res.first().cloned() {
684                Some(cmd) => cmd.into(),
685                _ => unreachable!(),
686            };
687            cmd.try_into().map_err(|_| {
688                FieldError::new(
689                    "Failed to convert transaction to the required type".to_string(),
690                    graphql_value!(null),
691                )
692            })
693        }
694        RpcTransactionInjectResponse::Rejected(rejected) => {
695            let error_list = rejected
696                .into_iter()
697                .map(|(_, err)| graphql_value!({ "message": err.to_string() }))
698                .collect::<Vec<_>>();
699
700            Err(FieldError::new(
701                "Transaction rejected",
702                graphql_value!(juniper::Value::List(error_list)),
703            ))
704        }
705        RpcTransactionInjectResponse::Failure(failure) => {
706            let error_list = failure
707                .into_iter()
708                .map(|err| graphql_value!({ "message": err.to_string() }))
709                .collect::<Vec<_>>();
710
711            Err(FieldError::new(
712                "Transaction failed",
713                graphql_value!(juniper::Value::List(error_list)),
714            ))
715        }
716    }
717}
718
719#[derive(Clone, Debug)]
720pub struct Mutation;
721
722/// GraphQL Mutation endpoints exposed by the Mina node
723///
724/// # Available Mutations:
725///
726/// ## Transaction Submission
727/// - `send_zkapp` - Submit a zkApp transaction to the network
728/// - `send_payment` - Send a payment transaction
729/// - `send_delegation` - Send a delegation transaction
730#[juniper::graphql_object(context = Context)]
731impl Mutation {
732    /// Submit a zkApp transaction to the network
733    ///
734    /// # Arguments
735    /// - `input`: zkApp command with account updates and fee payer information
736    ///
737    /// # Returns
738    /// Transaction response with hash and zkApp command details
739    async fn send_zkapp(
740        input: zkapp::SendZkappInput,
741        context: &Context,
742    ) -> juniper::FieldResult<zkapp::GraphQLSendZkappResponse> {
743        inject_tx(input.try_into()?, context).await
744    }
745
746    /// Send a payment transaction
747    ///
748    /// # Arguments
749    /// - `input`: Payment details including sender, receiver, amount, fee, and memo
750    /// - `signature`: Transaction signature
751    ///
752    /// # Returns
753    /// Payment response with transaction hash and details
754    async fn send_payment(
755        input: user_command::InputGraphQLPayment,
756        signature: user_command::UserCommandSignature,
757        context: &Context,
758    ) -> juniper::FieldResult<user_command::GraphQLSendPaymentResponse> {
759        // Grab the sender's account to get the infered nonce
760        let token_id = TokenIdKeyHash::default();
761        let public_key = AccountPublicKey::from_str(&input.from)
762            .map_err(|e| Error::Conversion(ConversionError::Base58Check(e)))?;
763
764        let accounts: Vec<Account> = context
765            .rpc_sender
766            .oneshot_request(RpcRequest::LedgerAccountsGet(
767                AccountQuery::PubKeyWithTokenId(public_key, token_id),
768            ))
769            .await
770            .ok_or(Error::StateMachineEmptyResponse)?;
771
772        let infered_nonce = accounts
773            .first()
774            .ok_or(Error::StateMachineEmptyResponse)?
775            .nonce;
776
777        let command = input
778            .create_user_command(infered_nonce, signature)
779            .map_err(Error::Conversion)?;
780
781        inject_tx(command, context).await
782    }
783
784    /// Send a delegation transaction
785    ///
786    /// # Arguments
787    /// - `input`: Delegation details including delegator, delegate, fee, and memo
788    /// - `signature`: Transaction signature
789    ///
790    /// # Returns
791    /// Delegation response with transaction hash and details
792    async fn send_delegation(
793        input: user_command::InputGraphQLDelegation,
794        signature: user_command::UserCommandSignature,
795        context: &Context,
796    ) -> juniper::FieldResult<user_command::GraphQLSendDelegationResponse> {
797        // Payment commands are always for the default (MINA) token
798        let token_id = TokenIdKeyHash::default();
799        let public_key = AccountPublicKey::from_str(&input.from)?;
800
801        // Grab the sender's account to get the infered nonce
802        let accounts: Vec<Account> = context
803            .rpc_sender
804            .oneshot_request(RpcRequest::LedgerAccountsGet(
805                AccountQuery::PubKeyWithTokenId(public_key, token_id),
806            ))
807            .await
808            .ok_or(Error::StateMachineEmptyResponse)?;
809
810        let infered_nonce = accounts
811            .first()
812            .ok_or(Error::StateMachineEmptyResponse)?
813            .nonce;
814        let command = input.create_user_command(infered_nonce, signature)?;
815
816        inject_tx(command, context).await
817    }
818}
819
820pub fn routes(
821    rpc_sernder: RpcSender,
822) -> impl Filter<Error = Rejection, Extract = impl Reply> + Clone {
823    let state = warp::any().map(move || Context::new(rpc_sernder.clone()));
824    let schema = RootNode::new(Query, Mutation, EmptySubscription::<Context>::new());
825    let graphql_filter = juniper_warp::make_graphql_filter(schema, state.boxed());
826    let graphiql_filter = juniper_warp::graphiql_filter("/graphql", None);
827    let playground_filter = juniper_warp::playground_filter("/graphql", None);
828
829    (warp::post().and(warp::path("graphql")).and(graphql_filter))
830        .or(warp::get()
831            .and(warp::path("playground"))
832            .and(playground_filter))
833        .or(warp::get().and(warp::path("graphiql")).and(graphiql_filter))
834}
835
836/// Helper function used by [`Query::pooled_user_commands`] and
837/// [`Query::pooled_zkapp_commands`] to parse public key, transaction hashes and
838/// command ids
839fn parse_pooled_commands_query<ID, F>(
840    public_key: Option<String>,
841    hashes: Option<Vec<String>>,
842    ids: Option<Vec<String>>,
843    id_map_fn: F,
844) -> Result<PooledCommandsQuery<ID>, ConversionError>
845where
846    F: Fn(&str) -> Result<ID, conv::Error>,
847{
848    let public_key = match public_key {
849        Some(public_key) => Some(AccountPublicKey::from_str(&public_key)?),
850        None => None,
851    };
852
853    let hashes = match hashes {
854        Some(hashes) => Some(
855            hashes
856                .into_iter()
857                .map(|tx| TransactionHash::from_str(tx.as_str()))
858                .collect::<Result<Vec<_>, _>>()?,
859        ),
860        None => None,
861    };
862
863    let ids = match ids {
864        Some(ids) => Some(
865            ids.into_iter()
866                .map(|id| id_map_fn(id.as_str()))
867                .collect::<Result<Vec<_>, _>>()?,
868        ),
869        None => None,
870    };
871
872    Ok(PooledCommandsQuery {
873        public_key,
874        hashes,
875        ids,
876    })
877}