1use account::{create_account_loader, AccountLoader, GraphQLAccount};
2use block::{GraphQLBlock, GraphQLSnarkJob, GraphQLUserCommands};
3use juniper::{graphql_value, FieldError, GraphQLEnum};
4use ledger::{Account, AccountId};
5use mina_core::{
6 block::AppliedBlock, consensus::ConsensusConstants, constants::constraint_constants,
7 NetworkConfig,
8};
9use mina_node::{
10 account::AccountPublicKey,
11 ledger::read::LedgerStatus,
12 rpc::{
13 AccountQuery, GetBlockQuery, PooledCommandsQuery, RpcBestChainResponse,
14 RpcGenesisBlockResponse, RpcGetBlockResponse, RpcLedgerAccountDelegatorsGetResponse,
15 RpcLedgerStatusGetResponse, RpcNodeStatus, RpcPooledUserCommandsResponse,
16 RpcPooledZkappCommandsResponse, RpcRequest, RpcSnarkPoolCompletedJobsResponse,
17 RpcSnarkPoolPendingJobsGetResponse, RpcSnarkerConfig, RpcStatusGetResponse,
18 RpcSyncStatsGetResponse, RpcTransactionInjectResponse, RpcTransactionStatusGetResponse,
19 SyncStatsQuery,
20 },
21 stats::sync::SyncKind,
22 BuildEnv,
23};
24use mina_node_common::rpc::RpcSender;
25use mina_p2p_messages::v2::{
26 conv, LedgerHash, MinaBaseSignedCommandStableV2, MinaBaseUserCommandStableV2,
27 MinaBaseZkappCommandTStableV1WireStableV1, TokenIdKeyHash, TransactionHash,
28};
29use mina_signer::CompressedPubKey;
30use o1_utils::field_helpers::FieldHelpersError;
31use snark::{GraphQLPendingSnarkWork, GraphQLSnarkWorker};
32use std::str::FromStr;
33use tokio::sync::OnceCell;
34use transaction::GraphQLTransactionStatus;
35use zkapp::GraphQLZkapp;
36
37pub mod account;
38pub mod block;
39pub mod constants;
40pub mod snark;
41pub mod transaction;
42pub mod user_command;
43pub mod zkapp;
44
45pub type GraphQLPublicKey = String;
47
48#[derive(Debug, thiserror::Error)]
49pub enum Error {
50 #[error("Conversion error: {0}")]
51 Conversion(ConversionError),
52 #[error("State machine empty response")]
53 StateMachineEmptyResponse,
54 #[error("Custom: {0}")]
55 Custom(String),
56}
57
58#[derive(Debug, thiserror::Error)]
59pub enum ConversionError {
60 #[error(transparent)]
61 Io(#[from] std::io::Error),
62 #[error(transparent)]
63 Conversion(#[from] mina_p2p_messages::v2::conv::Error),
64 #[error("Wrong variant")]
65 WrongVariant,
66 #[error("SerdeJson: {0}")]
67 SerdeJson(#[from] serde_json::Error),
68 #[error("Base58Check: {0}")]
69 Base58Check(#[from] mina_p2p_messages::b58::FromBase58CheckError),
70 #[error("Base58 error: {0}")]
71 Base58(#[from] bs58::decode::Error),
72 #[error(transparent)]
73 InvalidDecimalNumber(#[from] mina_p2p_messages::bigint::InvalidDecimalNumber),
74 #[error("Invalid bigint")]
75 InvalidBigInt,
76 #[error("Invalid hex")]
77 InvalidHex,
78 #[error(transparent)]
79 ParseInt(#[from] std::num::ParseIntError),
80 #[error(transparent)]
81 EnumParse(#[from] strum::ParseError),
82 #[error(transparent)]
83 TryFromInt(#[from] std::num::TryFromIntError),
84 #[error("Missing field: {0}")]
85 MissingField(String),
86 #[error("Invalid length")]
87 InvalidLength,
88 #[error("Custom: {0}")]
89 Custom(String),
90 #[error(transparent)]
91 FieldHelpers(#[from] FieldHelpersError),
92 #[error("Failed to convert integer to i32")]
93 Integer,
94}
95
96impl From<ConversionError> for Error {
97 fn from(value: ConversionError) -> Self {
98 Error::Conversion(value)
99 }
100}
101
102pub struct Context {
111 rpc_sender: RpcSender,
112 account_loader: AccountLoader,
113 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, Copy, Debug)]
202pub struct Query;
203
204#[juniper::graphql_object(context = Context)]
235impl Query {
236 async fn account(
245 public_key: String,
246 token: Option<String>,
247 context: &Context,
248 ) -> juniper::FieldResult<account::GraphQLAccount> {
249 let public_key = AccountPublicKey::from_str(&public_key)?;
250 let req = match token {
251 None => AccountQuery::SinglePublicKey(public_key),
252 Some(token) => {
253 let token_id = TokenIdKeyHash::from_str(&token)?;
254 AccountQuery::PubKeyWithTokenId(public_key, token_id)
255 }
256 };
257 let accounts: Vec<Account> = context
258 .rpc_sender
259 .oneshot_request(RpcRequest::LedgerAccountsGet(req))
260 .await
261 .ok_or(Error::StateMachineEmptyResponse)?;
262
263 Ok(accounts
264 .first()
265 .cloned()
266 .ok_or(Error::StateMachineEmptyResponse)?
267 .try_into()?)
268 }
269
270 async fn sync_status(context: &Context) -> juniper::FieldResult<SyncStatus> {
275 let state: RpcSyncStatsGetResponse = context
276 .rpc_sender
277 .oneshot_request(RpcRequest::SyncStatsGet(SyncStatsQuery { limit: Some(1) }))
278 .await
279 .ok_or(Error::StateMachineEmptyResponse)?;
280
281 if let Some(state) = state.as_ref().and_then(|s| s.first()) {
282 if state.synced.is_some() {
283 Ok(SyncStatus::SYNCED)
284 } else {
285 match &state.kind {
286 SyncKind::Bootstrap => Ok(SyncStatus::BOOTSTRAP),
287 SyncKind::Catchup => Ok(SyncStatus::CATCHUP),
288 }
289 }
290 } else {
291 Ok(SyncStatus::LISTENING)
292 }
293 }
294
295 async fn best_chain(
303 max_length: i32,
304 context: &Context,
305 ) -> juniper::FieldResult<Vec<GraphQLBlock>> {
306 let best_chain: Vec<AppliedBlock> = context
307 .rpc_sender
308 .oneshot_request(RpcRequest::BestChain(max_length as u32))
309 .await
310 .ok_or(Error::StateMachineEmptyResponse)?;
311
312 Ok(best_chain
313 .into_iter()
314 .map(|v| v.try_into())
315 .collect::<Result<Vec<_>, _>>()?)
316 }
317
318 async fn daemon_status(
323 _context: &Context,
324 ) -> juniper::FieldResult<constants::GraphQLDaemonStatus> {
325 Ok(constants::GraphQLDaemonStatus)
326 }
327
328 async fn genesis_constants(
333 context: &Context,
334 ) -> juniper::FieldResult<constants::GraphQLGenesisConstants> {
335 let consensus_constants: ConsensusConstants = context
336 .rpc_sender
337 .oneshot_request(RpcRequest::ConsensusConstantsGet)
338 .await
339 .ok_or(Error::StateMachineEmptyResponse)?;
340 let constraint_constants = constraint_constants();
341
342 Ok(constants::GraphQLGenesisConstants::try_new(
343 constraint_constants.clone(),
344 consensus_constants,
345 )?)
346 }
347
348 async fn transaction_status(
358 payment: Option<String>,
359 zkapp_transaction: Option<String>,
360 context: &Context,
361 ) -> juniper::FieldResult<GraphQLTransactionStatus> {
362 if payment.is_some() && zkapp_transaction.is_some() {
363 return Err(Error::Custom(
364 "Cannot provide both payment and zkapp transaction".to_string(),
365 )
366 .into());
367 }
368
369 let tx = if let Some(payment) = payment {
370 MinaBaseUserCommandStableV2::SignedCommand(MinaBaseSignedCommandStableV2::from_base64(
371 &payment,
372 )?)
373 } else if let Some(zkapp_transaction) = zkapp_transaction {
374 MinaBaseUserCommandStableV2::ZkappCommand(
375 MinaBaseZkappCommandTStableV1WireStableV1::from_base64(&zkapp_transaction)?,
376 )
377 } else {
378 return Err(Error::Custom(
379 "Must provide either payment or zkapp transaction".to_string(),
380 )
381 .into());
382 };
383 let res: RpcTransactionStatusGetResponse = context
384 .rpc_sender
385 .oneshot_request(RpcRequest::TransactionStatusGet(tx))
386 .await
387 .ok_or(Error::StateMachineEmptyResponse)?;
388
389 Ok(GraphQLTransactionStatus::from(res))
390 }
391
392 async fn block(
401 height: Option<i32>,
402 state_hash: Option<String>,
403 context: &Context,
404 ) -> juniper::FieldResult<GraphQLBlock> {
405 let query = match (height, state_hash) {
406 (Some(height), None) => GetBlockQuery::Height(height.try_into().unwrap_or(u32::MAX)),
407 (None, Some(state_hash)) => GetBlockQuery::Hash(state_hash.parse()?),
408 _ => {
409 return Err(Error::Custom(
410 "Must provide exactly one of state hash, height".to_owned(),
411 )
412 .into());
413 }
414 };
415
416 let res: Option<RpcGetBlockResponse> = context
417 .rpc_sender
418 .oneshot_request(RpcRequest::GetBlock(query.clone()))
419 .await;
420
421 match res {
422 None => Err(Error::Custom("response channel dropped".to_owned()).into()),
423 Some(None) => match query {
424 GetBlockQuery::Hash(hash) => Err(Error::Custom(format!(
425 "Could not find block with hash: `{}` in transition frontier",
426 hash
427 ))
428 .into()),
429 GetBlockQuery::Height(height) => Err(Error::Custom(format!(
430 "Could not find block with height: `{}` in transition frontier",
431 height
432 ))
433 .into()),
434 },
435 Some(Some(block)) => Ok(GraphQLBlock::try_from(block)?),
436 }
437 }
438
439 async fn pooled_user_commands(
448 &self,
449 public_key: Option<String>,
450 hashes: Option<Vec<String>>,
451 ids: Option<Vec<String>>,
452 context: &Context,
453 ) -> juniper::FieldResult<Vec<GraphQLUserCommands>> {
454 let query = parse_pooled_commands_query(
455 public_key,
456 hashes,
457 ids,
458 MinaBaseSignedCommandStableV2::from_base64,
459 )?;
460
461 let res: RpcPooledUserCommandsResponse = context
462 .rpc_sender
463 .oneshot_request(RpcRequest::PooledUserCommands(query))
464 .await
465 .ok_or(Error::StateMachineEmptyResponse)?;
466
467 Ok(res
468 .into_iter()
469 .map(GraphQLUserCommands::try_from)
470 .collect::<Result<Vec<_>, _>>()?)
471 }
472
473 async fn pooled_zkapp_commands(
482 public_key: Option<String>,
483 hashes: Option<Vec<String>>,
484 ids: Option<Vec<String>>,
485 context: &Context,
486 ) -> juniper::FieldResult<Vec<GraphQLZkapp>> {
487 let query = parse_pooled_commands_query(
488 public_key,
489 hashes,
490 ids,
491 MinaBaseZkappCommandTStableV1WireStableV1::from_base64,
492 )?;
493
494 let res: RpcPooledZkappCommandsResponse = context
495 .rpc_sender
496 .oneshot_request(RpcRequest::PooledZkappCommands(query))
497 .await
498 .ok_or(Error::StateMachineEmptyResponse)?;
499
500 Ok(res
501 .into_iter()
502 .map(GraphQLZkapp::try_from)
503 .collect::<Result<Vec<_>, _>>()?)
504 }
505
506 async fn genesis_block(context: &Context) -> juniper::FieldResult<GraphQLBlock> {
511 let block = context
512 .rpc_sender
513 .oneshot_request::<RpcGenesisBlockResponse>(RpcRequest::GenesisBlockGet)
514 .await
515 .ok_or(Error::StateMachineEmptyResponse)?
516 .ok_or(Error::StateMachineEmptyResponse)?;
517
518 Ok(GraphQLBlock::try_from(AppliedBlock {
519 block,
520 just_emitted_a_proof: false,
521 })?)
522 }
523
524 async fn snark_pool(context: &Context) -> juniper::FieldResult<Vec<GraphQLSnarkJob>> {
529 let jobs: RpcSnarkPoolCompletedJobsResponse = context
530 .rpc_sender
531 .oneshot_request(RpcRequest::SnarkPoolCompletedJobsGet)
532 .await
533 .ok_or(Error::StateMachineEmptyResponse)?;
534
535 Ok(jobs.iter().map(GraphQLSnarkJob::from).collect())
536 }
537
538 async fn pending_snark_work(
543 context: &Context,
544 ) -> juniper::FieldResult<Vec<GraphQLPendingSnarkWork>> {
545 let jobs: RpcSnarkPoolPendingJobsGetResponse = context
546 .rpc_sender
547 .oneshot_request(RpcRequest::SnarkPoolPendingJobsGet)
548 .await
549 .ok_or(Error::StateMachineEmptyResponse)?;
550
551 Ok(jobs
552 .into_iter()
553 .map(GraphQLPendingSnarkWork::try_from)
554 .collect::<Result<Vec<_>, _>>()?)
555 }
556
557 #[graphql(name = "networkID")]
562 async fn network_id(_context: &Context) -> juniper::FieldResult<String> {
563 let res = format!("mina:{}", NetworkConfig::global().name);
564 Ok(res)
565 }
566
567 async fn version(_context: &Context) -> juniper::FieldResult<String> {
572 let res = BuildEnv::get().git.commit_hash;
573 Ok(res)
574 }
575
576 async fn current_snark_worker(
581 &self,
582 context: &Context,
583 ) -> juniper::FieldResult<Option<GraphQLSnarkWorker>> {
584 let config: Option<RpcSnarkerConfig> = context
585 .rpc_sender
586 .oneshot_request(RpcRequest::SnarkerConfig)
587 .await
588 .ok_or(Error::StateMachineEmptyResponse)?;
589
590 let Some(config) = config else {
591 return Ok(None);
592 };
593
594 let account = context
595 .load_account(AccountId {
596 public_key: CompressedPubKey::try_from(&config.public_key)?,
597 token_id: TokenIdKeyHash::default().into(),
598 })
599 .await;
600
601 Ok(Some(GraphQLSnarkWorker {
602 key: config.public_key.to_string(),
603 account,
604 fee: config.fee.to_string(),
605 }))
606 }
607}
608
609async fn inject_tx<R>(
610 cmd: MinaBaseUserCommandStableV2,
611 context: &Context,
612) -> juniper::FieldResult<R>
613where
614 R: TryFrom<MinaBaseUserCommandStableV2>,
615{
616 let res: RpcTransactionInjectResponse = context
617 .rpc_sender
618 .oneshot_request(RpcRequest::TransactionInject(vec![cmd]))
619 .await
620 .ok_or(Error::StateMachineEmptyResponse)?;
621
622 match res {
623 RpcTransactionInjectResponse::Success(res) => {
624 let cmd: MinaBaseUserCommandStableV2 = match res.first().cloned() {
625 Some(cmd) => cmd.into(),
626 _ => unreachable!(),
627 };
628 cmd.try_into().map_err(|_| {
629 FieldError::new(
630 "Failed to convert transaction to the required type".to_string(),
631 graphql_value!(null),
632 )
633 })
634 }
635 RpcTransactionInjectResponse::Rejected(rejected) => {
636 let error_list = rejected
637 .into_iter()
638 .map(|(_, err)| graphql_value!({ "message": err.to_string() }))
639 .collect::<Vec<_>>();
640
641 Err(FieldError::new(
642 "Transaction rejected",
643 graphql_value!(juniper::Value::List(error_list)),
644 ))
645 }
646 RpcTransactionInjectResponse::Failure(failure) => {
647 let error_list = failure
648 .into_iter()
649 .map(|err| graphql_value!({ "message": err.to_string() }))
650 .collect::<Vec<_>>();
651
652 Err(FieldError::new(
653 "Transaction failed",
654 graphql_value!(juniper::Value::List(error_list)),
655 ))
656 }
657 }
658}
659
660#[derive(Clone, Debug)]
661pub struct Mutation;
662
663#[juniper::graphql_object(context = Context)]
672impl Mutation {
673 async fn send_zkapp(
681 input: zkapp::SendZkappInput,
682 context: &Context,
683 ) -> juniper::FieldResult<zkapp::GraphQLSendZkappResponse> {
684 inject_tx(input.try_into()?, context).await
685 }
686
687 async fn send_payment(
696 input: user_command::InputGraphQLPayment,
697 signature: user_command::UserCommandSignature,
698 context: &Context,
699 ) -> juniper::FieldResult<user_command::GraphQLSendPaymentResponse> {
700 let token_id = TokenIdKeyHash::default();
702 let public_key = AccountPublicKey::from_str(&input.from)
703 .map_err(|e| Error::Conversion(ConversionError::Base58Check(e)))?;
704
705 let accounts: Vec<Account> = context
706 .rpc_sender
707 .oneshot_request(RpcRequest::LedgerAccountsGet(
708 AccountQuery::PubKeyWithTokenId(public_key, token_id),
709 ))
710 .await
711 .ok_or(Error::StateMachineEmptyResponse)?;
712
713 let infered_nonce = accounts
714 .first()
715 .ok_or(Error::StateMachineEmptyResponse)?
716 .nonce;
717
718 let command = input
719 .create_user_command(infered_nonce, signature)
720 .map_err(Error::Conversion)?;
721
722 inject_tx(command, context).await
723 }
724
725 async fn send_delegation(
734 input: user_command::InputGraphQLDelegation,
735 signature: user_command::UserCommandSignature,
736 context: &Context,
737 ) -> juniper::FieldResult<user_command::GraphQLSendDelegationResponse> {
738 let token_id = TokenIdKeyHash::default();
740 let public_key = AccountPublicKey::from_str(&input.from)?;
741
742 let accounts: Vec<Account> = context
744 .rpc_sender
745 .oneshot_request(RpcRequest::LedgerAccountsGet(
746 AccountQuery::PubKeyWithTokenId(public_key, token_id),
747 ))
748 .await
749 .ok_or(Error::StateMachineEmptyResponse)?;
750
751 let infered_nonce = accounts
752 .first()
753 .ok_or(Error::StateMachineEmptyResponse)?
754 .nonce;
755 let command = input.create_user_command(infered_nonce, signature)?;
756
757 inject_tx(command, context).await
758 }
759}
760
761fn parse_pooled_commands_query<ID, F>(
765 public_key: Option<String>,
766 hashes: Option<Vec<String>>,
767 ids: Option<Vec<String>>,
768 id_map_fn: F,
769) -> Result<PooledCommandsQuery<ID>, ConversionError>
770where
771 F: Fn(&str) -> Result<ID, conv::Error>,
772{
773 let public_key = match public_key {
774 Some(public_key) => Some(AccountPublicKey::from_str(&public_key)?),
775 None => None,
776 };
777
778 let hashes = match hashes {
779 Some(hashes) => Some(
780 hashes
781 .into_iter()
782 .map(|tx| TransactionHash::from_str(tx.as_str()))
783 .collect::<Result<Vec<_>, _>>()?,
784 ),
785 None => None,
786 };
787
788 let ids = match ids {
789 Some(ids) => Some(
790 ids.into_iter()
791 .map(|id| id_map_fn(id.as_str()))
792 .collect::<Result<Vec<_>, _>>()?,
793 ),
794 None => None,
795 };
796
797 Ok(PooledCommandsQuery {
798 public_key,
799 hashes,
800 ids,
801 })
802}