Skip to main content

linera_sdk/test/
chain.rs

1// Copyright (c) Zefchain Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! A reference to a single microchain inside a [`TestValidator`].
5//!
6//! This allows manipulating a test microchain.
7
8use std::{
9    collections::HashMap,
10    io,
11    path::{Path, PathBuf},
12    sync::Arc,
13};
14
15use cargo_toml::Manifest;
16use futures::future;
17use linera_base::{
18    crypto::{AccountPublicKey, AccountSecretKey},
19    data_types::{
20        Amount, ApplicationDescription, Blob, BlockHeight, Bytecode, ChainDescription,
21        CompressedBytecode, Epoch,
22    },
23    identifiers::{AccountOwner, ApplicationId, ChainId, ModuleId, OwnerSpender},
24    vm::VmRuntime,
25};
26use linera_chain::{types::ConfirmedBlockCertificate, ChainExecutionContext};
27use linera_core::{data_types::ChainInfoQuery, worker::WorkerError};
28use linera_execution::{
29    system::{SystemOperation, SystemQuery, SystemResponse},
30    ExecutionError, Operation, Query, QueryOutcome, QueryResponse, ResourceTracker,
31};
32use linera_storage::Storage as _;
33use serde::Serialize;
34use tokio::{fs, sync::Mutex};
35
36use super::{BlockBuilder, TestValidator};
37use crate::{abis::fungible::FungibleTokenAbi, ContractAbi, ServiceAbi};
38
39/// A reference to a single microchain inside a [`TestValidator`].
40pub struct ActiveChain {
41    key_pair: AccountSecretKey,
42    description: ChainDescription,
43    tip: Arc<Mutex<Option<ConfirmedBlockCertificate>>>,
44    validator: TestValidator,
45}
46
47impl Clone for ActiveChain {
48    fn clone(&self) -> Self {
49        ActiveChain {
50            key_pair: self.key_pair.copy(),
51            description: self.description.clone(),
52            tip: self.tip.clone(),
53            validator: self.validator.clone(),
54        }
55    }
56}
57
58impl ActiveChain {
59    /// Creates a new [`ActiveChain`] instance referencing a new empty microchain in the
60    /// `validator`.
61    ///
62    /// The microchain has a single owner that uses the `key_pair` to produce blocks. The
63    /// `description` is used as the identifier of the microchain.
64    pub fn new(
65        key_pair: AccountSecretKey,
66        description: ChainDescription,
67        validator: TestValidator,
68    ) -> Self {
69        ActiveChain {
70            key_pair,
71            description,
72            tip: Arc::default(),
73            validator,
74        }
75    }
76
77    /// Returns the [`ChainId`] of this microchain.
78    pub fn id(&self) -> ChainId {
79        self.description.id()
80    }
81
82    /// Returns the [`AccountPublicKey`] of the active owner of this microchain.
83    pub fn public_key(&self) -> AccountPublicKey {
84        self.key_pair.public()
85    }
86
87    /// Returns the [`AccountSecretKey`] of the active owner of this microchain.
88    pub fn key_pair(&self) -> &AccountSecretKey {
89        &self.key_pair
90    }
91
92    /// Sets the [`AccountSecretKey`] to use for signing new blocks.
93    pub fn set_key_pair(&mut self, key_pair: AccountSecretKey) {
94        self.key_pair = key_pair
95    }
96
97    /// Returns the current [`Epoch`] the chain is in.
98    pub async fn epoch(&self) -> Epoch {
99        *Box::pin(self.validator.worker().chain_state_view(self.id()))
100            .await
101            .expect("Failed to load chain")
102            .execution_state
103            .system
104            .epoch
105            .get()
106    }
107
108    /// Reads the current shared balance available to all of the owners of this microchain.
109    pub async fn chain_balance(&self) -> Amount {
110        let query = Query::System(SystemQuery);
111
112        let (QueryOutcome { response, .. }, _) = self
113            .validator
114            .worker()
115            .query_application(self.id(), query, None)
116            .await
117            .expect("Failed to query chain's balance");
118
119        let QueryResponse::System(SystemResponse { balance, .. }) = response else {
120            panic!("Unexpected response from system application");
121        };
122
123        balance
124    }
125
126    /// Reads the current account balance on this microchain of an [`AccountOwner`].
127    pub async fn owner_balance(&self, owner: &AccountOwner) -> Option<Amount> {
128        let chain_state = Box::pin(self.validator.worker().chain_state_view(self.id()))
129            .await
130            .expect("Failed to read chain state");
131
132        chain_state
133            .execution_state
134            .system
135            .balances
136            .get(owner)
137            .await
138            .expect("Failed to read owner balance")
139    }
140
141    /// Reads the current account balance on this microchain of all [`AccountOwner`]s.
142    pub async fn owner_balances(
143        &self,
144        owners: impl IntoIterator<Item = AccountOwner>,
145    ) -> HashMap<AccountOwner, Option<Amount>> {
146        let chain_state = Box::pin(self.validator.worker().chain_state_view(self.id()))
147            .await
148            .expect("Failed to read chain state");
149
150        let mut balances = HashMap::new();
151
152        for owner in owners {
153            let balance = chain_state
154                .execution_state
155                .system
156                .balances
157                .get(&owner)
158                .await
159                .expect("Failed to read an owner's balance");
160
161            balances.insert(owner, balance);
162        }
163
164        balances
165    }
166
167    /// Reads a list of [`AccountOwner`]s that have a non-zero balance on this microchain.
168    pub async fn accounts(&self) -> Vec<AccountOwner> {
169        let chain_state = Box::pin(self.validator.worker().chain_state_view(self.id()))
170            .await
171            .expect("Failed to read chain state");
172
173        chain_state
174            .execution_state
175            .system
176            .balances
177            .indices()
178            .await
179            .expect("Failed to list accounts on the chain")
180    }
181
182    /// Reads all the non-zero account balances on this microchain.
183    pub async fn all_owner_balances(&self) -> HashMap<AccountOwner, Amount> {
184        self.owner_balances(self.accounts().await)
185            .await
186            .into_iter()
187            .map(|(owner, balance)| {
188                (
189                    owner,
190                    balance.expect("`accounts` should only return accounts with non-zero balance"),
191                )
192            })
193            .collect()
194    }
195
196    /// Adds a block to this microchain.
197    ///
198    /// The `block_builder` parameter is a closure that should use the [`BlockBuilder`] parameter
199    /// to provide the block's contents.
200    ///
201    /// Returns the block certificate and a [`ResourceTracker`] containing execution costs.
202    pub async fn add_block(
203        &self,
204        block_builder: impl FnOnce(&mut BlockBuilder),
205    ) -> (ConfirmedBlockCertificate, ResourceTracker) {
206        self.try_add_block(block_builder)
207            .await
208            .expect("Failed to execute block.")
209    }
210
211    /// Adds a block to this microchain, passing the blobs to be used during certificate handling.
212    ///
213    /// The `block_builder` parameter is a closure that should use the [`BlockBuilder`] parameter
214    /// to provide the block's contents.
215    ///
216    /// Returns the block certificate and a [`ResourceTracker`] containing execution costs.
217    pub async fn add_block_with_blobs(
218        &self,
219        block_builder: impl FnOnce(&mut BlockBuilder),
220        blobs: Vec<Blob>,
221    ) -> (ConfirmedBlockCertificate, ResourceTracker) {
222        self.try_add_block_with_blobs(block_builder, blobs)
223            .await
224            .expect("Failed to execute block.")
225    }
226
227    /// Tries to add a block to this microchain.
228    ///
229    /// The `block_builder` parameter is a closure that should use the [`BlockBuilder`] parameter
230    /// to provide the block's contents.
231    ///
232    /// Returns the block certificate and a [`ResourceTracker`] containing execution costs.
233    pub async fn try_add_block(
234        &self,
235        block_builder: impl FnOnce(&mut BlockBuilder),
236    ) -> Result<(ConfirmedBlockCertificate, ResourceTracker), WorkerError> {
237        self.try_add_block_with_blobs(block_builder, vec![]).await
238    }
239
240    /// Tries to add a block to this microchain, writing some `blobs` to storage if needed.
241    ///
242    /// The `block_builder` parameter is a closure that should use the [`BlockBuilder`] parameter
243    /// to provide the block's contents.
244    ///
245    /// The blobs are either all written to storage, if executing the block fails due to a missing
246    /// blob, or none are written to storage if executing the block succeeds without the blobs.
247    ///
248    /// Returns the block certificate and a [`ResourceTracker`] containing execution costs.
249    async fn try_add_block_with_blobs(
250        &self,
251        block_builder: impl FnOnce(&mut BlockBuilder),
252        blobs: Vec<Blob>,
253    ) -> Result<(ConfirmedBlockCertificate, ResourceTracker), WorkerError> {
254        let mut tip = self.tip.lock().await;
255        let mut block = BlockBuilder::new(
256            self.description.id(),
257            self.key_pair.public().into(),
258            Box::pin(self.epoch()).await,
259            tip.as_ref(),
260            self.validator.clone(),
261        );
262
263        block_builder(&mut block);
264
265        // TODO(#2066): Remove boxing once call-stack is shallower
266        let (certificate, resource_tracker) = Box::pin(block.try_sign(&blobs)).await?;
267
268        let result = self
269            .validator
270            .worker()
271            .fully_handle_certificate_with_notifications(certificate.clone(), &())
272            .await;
273        if let Err(WorkerError::BlobsNotFound(_)) = &result {
274            self.validator.storage().maybe_write_blobs(&blobs).await?;
275            self.validator
276                .worker()
277                .fully_handle_certificate_with_notifications(certificate.clone(), &())
278                .await
279                .expect("Rejected certificate");
280        } else {
281            result.expect("Rejected certificate");
282        }
283
284        *tip = Some(certificate.clone());
285
286        Ok((certificate, resource_tracker))
287    }
288
289    /// Receives all queued messages in all inboxes of this microchain.
290    ///
291    /// Adds a block to this microchain that receives all queued messages in the microchains
292    /// inboxes.
293    ///
294    /// Returns the certificate and resource tracker of the latest block added to the chain, if
295    /// any.
296    pub async fn handle_received_messages(
297        &self,
298    ) -> Option<(ConfirmedBlockCertificate, ResourceTracker)> {
299        let chain_id = self.id();
300        let information = self
301            .validator
302            .worker()
303            .handle_chain_info_query(ChainInfoQuery::new(chain_id).with_pending_message_bundles())
304            .await
305            .expect("Failed to query chain's pending messages");
306        let messages = information.info.requested_pending_message_bundles;
307        // Empty blocks are not allowed.
308        // Return early if there are no messages to process and we'd end up with an empty proposal.
309        if messages.is_empty() {
310            return None;
311        }
312        let result = Box::pin(self.add_block(|block| {
313            block.with_incoming_bundles(messages);
314        }))
315        .await;
316        Some(result)
317    }
318
319    /// Processes all new events from streams this chain subscribes to.
320    ///
321    /// Adds a block to this microchain that processes the new events.
322    ///
323    /// Returns the certificate and resource tracker of the block added to the chain.
324    pub async fn handle_new_events(&self) -> (ConfirmedBlockCertificate, ResourceTracker) {
325        let chain_id = self.id();
326        let worker = self.validator.worker();
327        let subscription_map = Box::pin(worker.chain_state_view(chain_id))
328            .await
329            .expect("Failed to query chain state view")
330            .execution_state
331            .system
332            .event_subscriptions
333            .index_values()
334            .await
335            .expect("Failed to query chain's event subscriptions");
336        let futures = subscription_map
337            .into_iter()
338            .map(|((chain_id, stream_id), subscriptions)| {
339                let worker = worker.clone();
340                async move {
341                    let counts = Box::pin(worker.chain_state_view(chain_id))
342                        .await
343                        .expect("Failed to query chain state view")
344                        .next_expected_events
345                        .get(&stream_id)
346                        .await
347                        .expect("Failed to query chain's event indices");
348                    let Some(counts) =
349                        counts.filter(|counts| counts.next_index > subscriptions.min_next_index)
350                    else {
351                        return Vec::new();
352                    };
353                    let first_index = counts.first_index;
354                    let next_index = counts.next_index;
355                    subscriptions
356                        .applications
357                        .into_iter()
358                        .filter(|(_, app_index)| *app_index < next_index)
359                        .map(|(application_id, _)| SystemOperation::UpdateStream {
360                            application_id,
361                            chain_id,
362                            stream_id: stream_id.clone(),
363                            first_index,
364                            next_index,
365                        })
366                        .collect::<Vec<_>>()
367                }
368            });
369        let updates: Vec<SystemOperation> = future::join_all(futures)
370            .await
371            .into_iter()
372            .flatten()
373            .collect();
374        assert!(!updates.is_empty(), "No new events to process");
375
376        Box::pin(self.add_block(|block| {
377            for update in updates {
378                block.with_system_operation(update);
379            }
380        }))
381        .await
382    }
383
384    /// Publishes the module in the crate calling this method to this microchain.
385    ///
386    /// Searches the Cargo manifest for binaries that end with `contract` and `service`, builds
387    /// them for WebAssembly and uses the generated binaries as the contract and service bytecode files
388    /// to be published on this chain. Returns the module ID to reference the published module.
389    pub async fn publish_current_module<Abi, Parameters, InstantiationArgument>(
390        &self,
391    ) -> ModuleId<Abi, Parameters, InstantiationArgument> {
392        Box::pin(self.publish_bytecode_files_in(".")).await
393    }
394
395    /// Publishes the bytecode files in the crate at `repository_path`.
396    ///
397    /// Searches the Cargo manifest for binaries that end with `contract` and `service`, builds
398    /// them for WebAssembly and uses the generated binaries as the contract and service bytecode files
399    /// to be published on this chain. Returns the module ID to reference the published module.
400    pub async fn publish_bytecode_files_in<Abi, Parameters, InstantiationArgument>(
401        &self,
402        repository_path: impl AsRef<Path>,
403    ) -> ModuleId<Abi, Parameters, InstantiationArgument> {
404        let repository_path = fs::canonicalize(repository_path)
405            .await
406            .expect("Failed to obtain absolute application repository path");
407        Self::build_bytecode_files_in(&repository_path);
408        let (contract, service) = Self::find_compressed_bytecode_files_in(&repository_path).await;
409        let contract_blob = Blob::new_contract_bytecode(contract);
410        let service_blob = Blob::new_service_bytecode(service);
411        let contract_blob_hash = contract_blob.id().hash;
412        let service_blob_hash = service_blob.id().hash;
413        let vm_runtime = VmRuntime::Wasm;
414
415        let module_id = ModuleId::new(contract_blob_hash, service_blob_hash, vm_runtime);
416
417        let (certificate, _) = Box::pin(self.add_block_with_blobs(
418            |block| {
419                block.with_system_operation(SystemOperation::PublishModule { module_id });
420            },
421            vec![contract_blob, service_blob],
422        ))
423        .await;
424
425        let block = certificate.inner().block();
426        assert_eq!(block.messages().len(), 1);
427        assert_eq!(block.messages()[0].len(), 0);
428
429        module_id.with_abi()
430    }
431
432    /// Compiles the crate in the `repository` path.
433    pub fn build_bytecode_files_in(repository: &Path) {
434        let output = std::process::Command::new("cargo")
435            .args(["build", "--release", "--target", "wasm32-unknown-unknown"])
436            .current_dir(repository)
437            .output()
438            .expect("Failed to build Wasm binaries");
439
440        assert!(
441            output.status.success(),
442            "Failed to build bytecode binaries.\nstdout: {}\nstderr: {}",
443            String::from_utf8_lossy(&output.stdout),
444            String::from_utf8_lossy(&output.stderr)
445        );
446    }
447
448    /// Searches the Cargo manifest of the crate calling this method for binaries to use as the
449    /// contract and service bytecode files.
450    ///
451    /// Returns a tuple with the loaded contract and service [`Bytecode`]s,
452    /// ready to be published.
453    pub async fn find_bytecode_files_in(repository: &Path) -> (Bytecode, Bytecode) {
454        let manifest_path = repository.join("Cargo.toml");
455        let cargo_manifest =
456            Manifest::from_path(manifest_path).expect("Failed to load Cargo.toml manifest");
457
458        let binaries = cargo_manifest
459            .bin
460            .into_iter()
461            .filter_map(|binary| binary.name)
462            .filter(|name| name.ends_with("service") || name.ends_with("contract"))
463            .collect::<Vec<_>>();
464
465        assert_eq!(
466            binaries.len(),
467            2,
468            "Could not figure out contract and service bytecode binaries.\
469            Please specify them manually using `publish_module`."
470        );
471
472        let (contract_binary, service_binary) = if binaries[0].ends_with("contract") {
473            (&binaries[0], &binaries[1])
474        } else {
475            (&binaries[1], &binaries[0])
476        };
477
478        let base_path = Self::find_output_directory_of(repository)
479            .await
480            .expect("Failed to look for output binaries");
481        let contract_path = base_path.join(format!("{contract_binary}.wasm"));
482        let service_path = base_path.join(format!("{service_binary}.wasm"));
483
484        let contract = Bytecode::load_from_file(contract_path)
485            .await
486            .expect("Failed to load contract bytecode from file");
487        let service = Bytecode::load_from_file(service_path)
488            .await
489            .expect("Failed to load service bytecode from file");
490        (contract, service)
491    }
492
493    /// Returns a tuple with the loaded contract and service [`CompressedBytecode`]s,
494    /// ready to be published.
495    pub async fn find_compressed_bytecode_files_in(
496        repository: &Path,
497    ) -> (CompressedBytecode, CompressedBytecode) {
498        let (contract, service) = Self::find_bytecode_files_in(repository).await;
499        tokio::task::spawn_blocking(move || (contract.compress(), service.compress()))
500            .await
501            .expect("Failed to compress bytecode files")
502    }
503
504    /// Searches for the directory where the built WebAssembly binaries should be.
505    ///
506    /// Assumes that the binaries will be built and placed inside a
507    /// `target/wasm32-unknown-unknown/release` sub-directory. However, since the crate with the
508    /// binaries could be part of a workspace, that output sub-directory must be searched in parent
509    /// directories as well.
510    async fn find_output_directory_of(repository: &Path) -> Result<PathBuf, io::Error> {
511        let output_sub_directory = Path::new("target/wasm32-unknown-unknown/release");
512        let mut current_directory = repository;
513        let mut output_path = current_directory.join(output_sub_directory);
514
515        while !fs::try_exists(&output_path).await? {
516            current_directory = current_directory.parent().unwrap_or_else(|| {
517                panic!(
518                    "Failed to find Wasm binary output directory in {}",
519                    repository.display()
520                )
521            });
522
523            output_path = current_directory.join(output_sub_directory);
524        }
525
526        Ok(output_path)
527    }
528
529    /// Returns the height of the tip of this microchain.
530    pub async fn get_tip_height(&self) -> BlockHeight {
531        self.tip
532            .lock()
533            .await
534            .as_ref()
535            .expect("Block was not successfully added")
536            .inner()
537            .block()
538            .header
539            .height
540    }
541
542    /// Creates an application on this microchain, using the module referenced by `module_id`.
543    ///
544    /// Returns the [`ApplicationId`] of the created application.
545    ///
546    /// If necessary, this microchain will subscribe to the microchain that published the
547    /// module to use, and fetch it.
548    ///
549    /// The application is instantiated using the instantiation parameters, which consist of the
550    /// global static `parameters`, the one time `instantiation_argument` and the
551    /// `required_application_ids` of the applications that the new application will depend on.
552    pub async fn create_application<Abi, Parameters, InstantiationArgument>(
553        &mut self,
554        module_id: ModuleId<Abi, Parameters, InstantiationArgument>,
555        parameters: Parameters,
556        instantiation_argument: InstantiationArgument,
557        required_application_ids: Vec<ApplicationId>,
558    ) -> ApplicationId<Abi>
559    where
560        Abi: ContractAbi,
561        Parameters: Serialize,
562        InstantiationArgument: Serialize,
563    {
564        let parameters = serde_json::to_vec(&parameters).unwrap();
565        let instantiation_argument = serde_json::to_vec(&instantiation_argument).unwrap();
566
567        let (creation_certificate, _) = Box::pin(self.add_block(|block| {
568            block.with_system_operation(SystemOperation::CreateApplication {
569                module_id: module_id.forget_abi(),
570                parameters: parameters.clone(),
571                instantiation_argument,
572                required_application_ids: required_application_ids.clone(),
573            });
574        }))
575        .await;
576
577        let block = creation_certificate.inner().block();
578        assert_eq!(block.messages().len(), 1);
579
580        let description = ApplicationDescription {
581            module_id: module_id.forget_abi(),
582            creator_chain_id: block.header.chain_id,
583            block_height: block.header.height,
584            application_index: 0,
585            parameters,
586            required_application_ids,
587        };
588
589        ApplicationId::<()>::from(&description).with_abi()
590    }
591
592    /// Fallible version of [`create_application`](Self::create_application).
593    ///
594    /// Returns the [`ApplicationId`] on success, or a [`WorkerError`] if instantiation fails.
595    pub async fn try_create_application<Abi, Parameters, InstantiationArgument>(
596        &mut self,
597        module_id: ModuleId<Abi, Parameters, InstantiationArgument>,
598        parameters: Parameters,
599        instantiation_argument: InstantiationArgument,
600        required_application_ids: Vec<ApplicationId>,
601    ) -> Result<ApplicationId<Abi>, WorkerError>
602    where
603        Abi: ContractAbi,
604        Parameters: Serialize,
605        InstantiationArgument: Serialize,
606    {
607        let parameters = serde_json::to_vec(&parameters).unwrap();
608        let instantiation_argument = serde_json::to_vec(&instantiation_argument).unwrap();
609
610        let (creation_certificate, _) = self
611            .try_add_block(|block| {
612                block.with_system_operation(SystemOperation::CreateApplication {
613                    module_id: module_id.forget_abi(),
614                    parameters: parameters.clone(),
615                    instantiation_argument,
616                    required_application_ids: required_application_ids.clone(),
617                });
618            })
619            .await?;
620
621        let block = creation_certificate.inner().block();
622
623        let description = ApplicationDescription {
624            module_id: module_id.forget_abi(),
625            creator_chain_id: block.header.chain_id,
626            block_height: block.header.height,
627            application_index: 0,
628            parameters,
629            required_application_ids,
630        };
631
632        Ok(ApplicationId::<()>::from(&description).with_abi())
633    }
634
635    /// Returns whether this chain has been closed.
636    pub async fn is_closed(&self) -> bool {
637        let chain = Box::pin(self.validator.worker().chain_state_view(self.id()))
638            .await
639            .expect("Failed to load chain");
640        *chain.execution_state.system.closed.get()
641    }
642
643    /// Executes a `query` on an `application`'s state on this microchain.
644    ///
645    /// Returns the deserialized response from the `application`.
646    pub async fn query<Abi>(
647        &self,
648        application_id: ApplicationId<Abi>,
649        query: Abi::Query,
650    ) -> QueryOutcome<Abi::QueryResponse>
651    where
652        Abi: ServiceAbi,
653    {
654        self.try_query(application_id, query)
655            .await
656            .expect("Failed to execute application service query")
657    }
658
659    /// Attempts to execute a `query` on an `application`'s state on this microchain.
660    ///
661    /// Returns the deserialized response from the `application`.
662    pub async fn try_query<Abi>(
663        &self,
664        application_id: ApplicationId<Abi>,
665        query: Abi::Query,
666    ) -> Result<QueryOutcome<Abi::QueryResponse>, TryQueryError>
667    where
668        Abi: ServiceAbi,
669    {
670        let query_bytes = serde_json::to_vec(&query)?;
671
672        let (
673            QueryOutcome {
674                response,
675                operations,
676            },
677            _,
678        ) = self
679            .validator
680            .worker()
681            .query_application(
682                self.id(),
683                Query::User {
684                    application_id: application_id.forget_abi(),
685                    bytes: query_bytes,
686                },
687                None,
688            )
689            .await?;
690
691        let deserialized_response = match response {
692            QueryResponse::User(bytes) => {
693                serde_json::from_slice(&bytes).expect("Failed to deserialize query response")
694            }
695            QueryResponse::System(_) => {
696                unreachable!("User query returned a system response")
697            }
698        };
699
700        Ok(QueryOutcome {
701            response: deserialized_response,
702            operations,
703        })
704    }
705
706    /// Executes a GraphQL `query` on an `application`'s state on this microchain.
707    ///
708    /// Returns the deserialized GraphQL JSON response from the `application`.
709    pub async fn graphql_query<Abi>(
710        &self,
711        application_id: ApplicationId<Abi>,
712        query: impl Into<async_graphql::Request>,
713    ) -> QueryOutcome<serde_json::Value>
714    where
715        Abi: ServiceAbi<Query = async_graphql::Request, QueryResponse = async_graphql::Response>,
716    {
717        let query = query.into();
718        let query_str = query.query.clone();
719
720        self.try_graphql_query(application_id, query)
721            .await
722            .unwrap_or_else(|error| panic!("Service query {query_str:?} failed: {error}"))
723    }
724
725    /// Attempts to execute a GraphQL `query` on an `application`'s state on this microchain.
726    ///
727    /// Returns the deserialized GraphQL JSON response from the `application`.
728    pub async fn try_graphql_query<Abi>(
729        &self,
730        application_id: ApplicationId<Abi>,
731        query: impl Into<async_graphql::Request>,
732    ) -> Result<QueryOutcome<serde_json::Value>, TryGraphQLQueryError>
733    where
734        Abi: ServiceAbi<Query = async_graphql::Request, QueryResponse = async_graphql::Response>,
735    {
736        let query = query.into();
737        let QueryOutcome {
738            response,
739            operations,
740        } = self.try_query(application_id, query).await?;
741
742        if !response.errors.is_empty() {
743            return Err(TryGraphQLQueryError::Service(response.errors));
744        }
745        let json_response = response.data.into_json()?;
746
747        Ok(QueryOutcome {
748            response: json_response,
749            operations,
750        })
751    }
752
753    /// Executes a GraphQL `mutation` on an `application` and proposes a block with the resulting
754    /// scheduled operations.
755    ///
756    /// Returns the certificate of the new block.
757    pub async fn graphql_mutation<Abi>(
758        &self,
759        application_id: ApplicationId<Abi>,
760        query: impl Into<async_graphql::Request>,
761    ) -> ConfirmedBlockCertificate
762    where
763        Abi: ServiceAbi<Query = async_graphql::Request, QueryResponse = async_graphql::Response>,
764    {
765        self.try_graphql_mutation(application_id, query)
766            .await
767            .expect("Failed to execute service GraphQL mutation")
768    }
769
770    /// Attempts to execute a GraphQL `mutation` on an `application` and proposes a block with the
771    /// resulting scheduled operations.
772    ///
773    /// Returns the certificate of the new block.
774    pub async fn try_graphql_mutation<Abi>(
775        &self,
776        application_id: ApplicationId<Abi>,
777        query: impl Into<async_graphql::Request>,
778    ) -> Result<ConfirmedBlockCertificate, TryGraphQLMutationError>
779    where
780        Abi: ServiceAbi<Query = async_graphql::Request, QueryResponse = async_graphql::Response>,
781    {
782        let QueryOutcome { operations, .. } = self.try_graphql_query(application_id, query).await?;
783
784        let (certificate, _) = Box::pin(self.try_add_block(|block| {
785            for operation in operations {
786                match operation {
787                    Operation::User {
788                        application_id,
789                        bytes,
790                    } => {
791                        block.with_raw_operation(application_id, bytes);
792                    }
793                    Operation::System(system_operation) => {
794                        block.with_system_operation(*system_operation);
795                    }
796                }
797            }
798        }))
799        .await?;
800
801        Ok(certificate)
802    }
803
804    /// Queries the balance of an account owned by `account_owner` on this chain.
805    pub async fn query_account(
806        &self,
807        application_id: ApplicationId<FungibleTokenAbi>,
808        account_owner: AccountOwner,
809    ) -> Option<Amount> {
810        use async_graphql::InputType as _;
811
812        let query = format!(
813            "query {{ accounts {{ entry(key: {}) {{ value }} }} }}",
814            account_owner.to_value()
815        );
816        let QueryOutcome { response, .. } = self.graphql_query(application_id, query).await;
817        let balance = response.pointer("/accounts/entry/value")?.as_str()?;
818
819        Some(
820            balance
821                .parse()
822                .expect("Account balance cannot be parsed as a number"),
823        )
824    }
825
826    /// Queries the allowance for an owner-spender pair on this chain.
827    pub async fn query_allowance(
828        &self,
829        application_id: ApplicationId<FungibleTokenAbi>,
830        owner: AccountOwner,
831        spender: AccountOwner,
832    ) -> Option<Amount> {
833        use async_graphql::InputType as _;
834
835        let owner_spender = OwnerSpender::new(owner, spender);
836        let query = format!(
837            "query {{ allowances {{ entry(key: {}) {{ value }} }} }}",
838            owner_spender.to_value()
839        );
840        let QueryOutcome { response, .. } = self.graphql_query(application_id, query).await;
841        let allowance = response.pointer("/allowances/entry/value")?.as_str()?;
842
843        Some(
844            allowance
845                .parse()
846                .expect("Allowance cannot be parsed as a number"),
847        )
848    }
849}
850
851/// Failure to query an application's service on a chain.
852#[derive(Debug, thiserror::Error)]
853pub enum TryQueryError {
854    /// The query request failed to serialize to JSON.
855    #[error("Failed to serialize query request")]
856    Serialization(#[from] serde_json::Error),
857
858    /// Executing the service to handle the query failed.
859    #[error("Failed to execute service query")]
860    Execution(#[from] WorkerError),
861}
862
863/// Failure to perform a GraphQL query on an application on a chain.
864#[derive(Debug, thiserror::Error)]
865pub enum TryGraphQLQueryError {
866    /// The [`async_graphql::Request`] failed to serialize to JSON.
867    #[error("Failed to serialize GraphQL query request")]
868    RequestSerialization(#[source] serde_json::Error),
869
870    /// Execution of the service failed.
871    #[error("Failed to execute service query")]
872    Execution(#[from] WorkerError),
873
874    /// The response returned from the service was not valid JSON.
875    #[error("Unexpected non-JSON service query response")]
876    ResponseDeserialization(#[from] serde_json::Error),
877
878    /// The service reported some errors.
879    #[error("Service returned errors: {_0:#?}")]
880    Service(Vec<async_graphql::ServerError>),
881}
882
883impl From<TryQueryError> for TryGraphQLQueryError {
884    fn from(query_error: TryQueryError) -> Self {
885        match query_error {
886            TryQueryError::Serialization(error) => {
887                TryGraphQLQueryError::RequestSerialization(error)
888            }
889            TryQueryError::Execution(error) => TryGraphQLQueryError::Execution(error),
890        }
891    }
892}
893
894impl TryGraphQLQueryError {
895    /// Returns the inner [`ExecutionError`] in this error.
896    ///
897    /// # Panics
898    ///
899    /// If this is not caused by an [`ExecutionError`].
900    pub fn expect_execution_error(self) -> ExecutionError {
901        let TryGraphQLQueryError::Execution(worker_error) = self else {
902            panic!("Expected an `ExecutionError`. Got: {self:#?}");
903        };
904
905        worker_error.expect_execution_error(ChainExecutionContext::Query)
906    }
907}
908
909/// Failure to perform a GraphQL mutation on an application on a chain.
910#[derive(Debug, thiserror::Error)]
911pub enum TryGraphQLMutationError {
912    /// The GraphQL query for the mutation failed.
913    #[error(transparent)]
914    Query(#[from] TryGraphQLQueryError),
915
916    /// The block with the mutation's scheduled operations failed to be proposed.
917    #[error("Failed to propose block with operations scheduled by the GraphQL mutation")]
918    Proposal(#[from] WorkerError),
919}
920
921impl TryGraphQLMutationError {
922    /// Returns the inner [`ExecutionError`] in this [`TryGraphQLMutationError::Proposal`] error.
923    ///
924    /// # Panics
925    ///
926    /// If this is not caused by an [`ExecutionError`] during a block proposal.
927    pub fn expect_proposal_execution_error(self, transaction_index: u32) -> ExecutionError {
928        let TryGraphQLMutationError::Proposal(proposal_error) = self else {
929            panic!("Expected an `ExecutionError` during the block proposal. Got: {self:#?}");
930        };
931
932        proposal_error.expect_execution_error(ChainExecutionContext::Operation(transaction_index))
933    }
934}