Skip to main content

iota_sdk_graphql_client/api/
transactions.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2026 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5//! Transactions API implementation.
6
7use std::time::Duration;
8
9use base64ct::Encoding;
10use cynic::{MutationBuilder, QueryBuilder};
11use futures::Stream;
12use iota_transaction_builder::WaitForTransaction;
13use iota_types::{
14    Address, SenderSignedTransaction, SignedTransaction, Transaction, TransactionDigest,
15    TransactionEffects, UserSignature,
16};
17
18use crate::{
19    GraphQLClient, TransactionDataEffects,
20    api::define_query,
21    error::{GraphQLError, GraphQLResult},
22    pagination::{Page, PaginationFilter, PaginationFilterResponse},
23    query_types::{
24        AddressTransactionBlocksQueryFragment, AddressTransactionRelationship,
25        AddressTransactionsQueryArgs, AddressTransactionsQueryFragment, ExecuteTransactionArgs,
26        ExecuteTransactionQueryFragment, TransactionBlockArgs,
27        TransactionBlockCheckpointQueryFragment, TransactionBlockEffectsQueryFragment,
28        TransactionBlockIndexedQueryFragment, TransactionBlockQueryFragment,
29        TransactionBlockWithEffectsQueryFragment, TransactionBlocksEffectsQueryFragment,
30        TransactionBlocksQueryArgs, TransactionBlocksQueryFragment,
31        TransactionBlocksWithEffectsQueryFragment, TransactionsFilter,
32    },
33    streams::stream_paginated_query,
34};
35
36define_query! {
37    /// Query for [`GraphQLClient::transactions`]. Await it to send the
38    /// request.
39    #[derive(Clone)]
40    pub struct ListTransactionsQuery {
41        client: GraphQLClient,
42        filter: Option<TransactionsFilter>,
43        pagination: PaginationFilter,
44    }
45    output: GraphQLResult<Page<SignedTransaction>>;
46}
47
48impl ListTransactionsQuery {
49    /// Only return the transactions that match `filter`.
50    pub fn filter(mut self, filter: TransactionsFilter) -> Self {
51        self.filter = Some(filter);
52        self
53    }
54
55    /// Set the page to fetch.
56    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
57        self.pagination = pagination;
58        self
59    }
60
61    /// Stream every item, page by page, starting at the pagination's cursor
62    /// and in its direction, with its limit as the page size.
63    pub fn stream(self) -> impl Stream<Item = GraphQLResult<SignedTransaction>> + Unpin {
64        let pagination = self.pagination.clone();
65        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
66    }
67
68    fn operation(
69        filter: Option<TransactionsFilter>,
70        pagination: PaginationFilterResponse,
71    ) -> cynic::Operation<TransactionBlocksQueryFragment, TransactionBlocksQueryArgs> {
72        TransactionBlocksQueryFragment::build(TransactionBlocksQueryArgs {
73            after: pagination.after,
74            before: pagination.before,
75            filter: filter.map(Into::into),
76            first: pagination.first,
77            last: pagination.last,
78        })
79    }
80
81    async fn send(self) -> GraphQLResult<Page<SignedTransaction>> {
82        let Self {
83            client,
84            pagination,
85            filter,
86        } = self;
87        let pagination = client.pagination_filter(pagination).await;
88        let response = client
89            .run_query(&Self::operation(filter, pagination))
90            .await?;
91
92        let txc = response.transaction_blocks;
93        let page_info = txc.page_info;
94
95        let transactions = txc
96            .nodes
97            .into_iter()
98            .map(|n| n.try_into())
99            .collect::<GraphQLResult<Vec<_>>>()?;
100        Ok(Page::new(page_info, transactions))
101    }
102}
103
104define_query! {
105    /// Query for [`GraphQLClient::address_transactions`]. Await it to send the
106    /// request.
107    #[derive(Clone)]
108    pub struct ListAddressTransactionsQuery {
109        client: GraphQLClient,
110        address: Address,
111        relation: Option<AddressTransactionRelationship>,
112        filter: Option<TransactionsFilter>,
113        pagination: PaginationFilter,
114    }
115    output: GraphQLResult<Page<SignedTransaction>>;
116}
117
118impl ListAddressTransactionsQuery {
119    /// Set how the address relates to the transactions. Defaults to the
120    /// transactions it sent.
121    pub fn relation(mut self, relation: AddressTransactionRelationship) -> Self {
122        self.relation = Some(relation);
123        self
124    }
125
126    /// Only return the transactions that match `filter`.
127    pub fn filter(mut self, filter: TransactionsFilter) -> Self {
128        self.filter = Some(filter);
129        self
130    }
131
132    /// Set the page to fetch.
133    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
134        self.pagination = pagination;
135        self
136    }
137
138    /// Stream every item, page by page, starting at the pagination's cursor
139    /// and in its direction, with its limit as the page size.
140    pub fn stream(self) -> impl Stream<Item = GraphQLResult<SignedTransaction>> + Unpin {
141        let pagination = self.pagination.clone();
142        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
143    }
144
145    fn operation(
146        address: Address,
147        relation: Option<AddressTransactionRelationship>,
148        filter: Option<TransactionsFilter>,
149        pagination: PaginationFilterResponse,
150    ) -> cynic::Operation<AddressTransactionsQueryFragment, AddressTransactionsQueryArgs> {
151        AddressTransactionsQueryFragment::build(AddressTransactionsQueryArgs {
152            address,
153            relation,
154            after: pagination.after,
155            before: pagination.before,
156            filter: filter.map(Into::into),
157            first: pagination.first,
158            last: pagination.last,
159        })
160    }
161
162    async fn send(self) -> GraphQLResult<Page<SignedTransaction>> {
163        let Self {
164            client,
165            pagination,
166            address,
167            relation,
168            filter,
169        } = self;
170        let pagination = client.pagination_filter(pagination).await;
171        let response = client
172            .run_query(&Self::operation(address, relation, filter, pagination))
173            .await?;
174
175        let Some(AddressTransactionBlocksQueryFragment { transaction_blocks }) = response.address
176        else {
177            return Ok(Page::new_empty());
178        };
179
180        let transactions = transaction_blocks
181            .nodes
182            .into_iter()
183            .map(|n| n.try_into())
184            .collect::<GraphQLResult<Vec<_>>>()?;
185
186        Ok(Page::new(transaction_blocks.page_info, transactions))
187    }
188}
189
190define_query! {
191    /// Query for [`GraphQLClient::transactions_effects`]. Await it to send the
192    /// request.
193    #[derive(Clone)]
194    pub struct ListTransactionsEffectsQuery {
195        client: GraphQLClient,
196        filter: Option<TransactionsFilter>,
197        pagination: PaginationFilter,
198    }
199    output: GraphQLResult<Page<TransactionEffects>>;
200}
201
202impl ListTransactionsEffectsQuery {
203    /// Only return the transactions that match `filter`.
204    pub fn filter(mut self, filter: TransactionsFilter) -> Self {
205        self.filter = Some(filter);
206        self
207    }
208
209    /// Set the page to fetch.
210    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
211        self.pagination = pagination;
212        self
213    }
214
215    /// Stream every item, page by page, starting at the pagination's cursor
216    /// and in its direction, with its limit as the page size.
217    pub fn stream(self) -> impl Stream<Item = GraphQLResult<TransactionEffects>> + Unpin {
218        let pagination = self.pagination.clone();
219        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
220    }
221
222    fn operation(
223        filter: Option<TransactionsFilter>,
224        pagination: PaginationFilterResponse,
225    ) -> cynic::Operation<TransactionBlocksEffectsQueryFragment, TransactionBlocksQueryArgs> {
226        TransactionBlocksEffectsQueryFragment::build(TransactionBlocksQueryArgs {
227            after: pagination.after,
228            before: pagination.before,
229            filter: filter.map(Into::into),
230            first: pagination.first,
231            last: pagination.last,
232        })
233    }
234
235    async fn send(self) -> GraphQLResult<Page<TransactionEffects>> {
236        let Self {
237            client,
238            pagination,
239            filter,
240        } = self;
241        let pagination = client.pagination_filter(pagination).await;
242        let response = client
243            .run_query(&Self::operation(filter, pagination))
244            .await?;
245
246        let txc = response.transaction_blocks;
247        let page_info = txc.page_info;
248
249        let transactions = txc
250            .nodes
251            .into_iter()
252            .map(|n| n.try_into())
253            .collect::<GraphQLResult<Vec<_>>>()?;
254        Ok(Page::new(page_info, transactions))
255    }
256}
257
258define_query! {
259    /// Query for [`GraphQLClient::transactions_data_effects`]. Await it to
260    /// send the request.
261    #[derive(Clone)]
262    pub struct ListTransactionsDataEffectsQuery {
263        client: GraphQLClient,
264        filter: Option<TransactionsFilter>,
265        pagination: PaginationFilter,
266    }
267    output: GraphQLResult<Page<TransactionDataEffects>>;
268}
269
270impl ListTransactionsDataEffectsQuery {
271    /// Only return the transactions that match `filter`.
272    pub fn filter(mut self, filter: TransactionsFilter) -> Self {
273        self.filter = Some(filter);
274        self
275    }
276
277    /// Set the page to fetch.
278    pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
279        self.pagination = pagination;
280        self
281    }
282
283    /// Stream every item, page by page, starting at the pagination's cursor
284    /// and in its direction, with its limit as the page size.
285    pub fn stream(self) -> impl Stream<Item = GraphQLResult<TransactionDataEffects>> + Unpin {
286        let pagination = self.pagination.clone();
287        stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
288    }
289
290    fn operation(
291        filter: Option<TransactionsFilter>,
292        pagination: PaginationFilterResponse,
293    ) -> cynic::Operation<TransactionBlocksWithEffectsQueryFragment, TransactionBlocksQueryArgs>
294    {
295        TransactionBlocksWithEffectsQueryFragment::build(TransactionBlocksQueryArgs {
296            after: pagination.after,
297            before: pagination.before,
298            filter: filter.map(Into::into),
299            first: pagination.first,
300            last: pagination.last,
301        })
302    }
303
304    async fn send(self) -> GraphQLResult<Page<TransactionDataEffects>> {
305        let Self {
306            client,
307            pagination,
308            filter,
309        } = self;
310        let pagination = client.pagination_filter(pagination).await;
311        let response = client
312            .run_query(&Self::operation(filter, pagination))
313            .await?;
314
315        let txc = response.transaction_blocks;
316        let page_info = txc.page_info;
317
318        let transactions = {
319            txc.nodes
320                .into_iter()
321                .map(|node| {
322                    let (Some(bcs), Some(effects)) = (node.bcs, node.effects) else {
323                        return Err(GraphQLError::EmptyResponseField(
324                            "transaction bcs or effects",
325                        ));
326                    };
327                    let bcs = crate::base64::decode(bcs.0.as_str())?;
328                    let effects = crate::base64::decode(effects.bcs.as_ref().unwrap().0.as_str())?;
329                    let transaction: SenderSignedTransaction =
330                        bcs::from_bytes(&bcs).map_err(iota_types::BcsError::new)?;
331                    let effects: TransactionEffects =
332                        bcs::from_bytes(&effects).map_err(iota_types::BcsError::new)?;
333
334                    Ok(TransactionDataEffects {
335                        signed_transaction: transaction.into(),
336                        effects,
337                    })
338                })
339                .collect::<GraphQLResult<Vec<_>>>()?
340        };
341
342        Ok(Page::new(page_info, transactions))
343    }
344}
345
346define_query! {
347    /// Query for [`GraphQLClient::execute_transaction`]. Await it to send the
348    /// request.
349    pub struct ExecuteTransactionQuery {
350        client: GraphQLClient,
351        signatures: Vec<String>,
352        transaction: Transaction,
353        wait_for: Option<WaitForTransaction>,
354    }
355    output: GraphQLResult<TransactionEffects>;
356}
357
358impl ExecuteTransactionQuery {
359    /// Wait for the executed transaction to be indexed or finalized before
360    /// resolving.
361    pub fn wait_for(mut self, wait_for: WaitForTransaction) -> Self {
362        self.wait_for = Some(wait_for);
363        self
364    }
365
366    async fn send(self) -> GraphQLResult<TransactionEffects> {
367        let operation = ExecuteTransactionQueryFragment::build(ExecuteTransactionArgs {
368            signatures: self.signatures,
369            tx_bytes: base64ct::Base64::encode_string(
370                bcs::to_bytes(&self.transaction).unwrap().as_ref(),
371            ),
372        });
373
374        let response = self.client.run_query(&operation).await?;
375
376        let result = response.execute_transaction_block;
377        let bcs = crate::base64::decode(result.effects.bcs.0.as_str())?;
378        let effects: TransactionEffects =
379            bcs::from_bytes(&bcs).map_err(iota_types::BcsError::new)?;
380
381        if let Some(wait_for) = self.wait_for {
382            self.client
383                .wait_for_transaction(self.transaction.digest(), wait_for)
384                .await?;
385        }
386
387        Ok(effects)
388    }
389}
390
391define_query! {
392    /// Query for [`GraphQLClient::wait_for_transaction`]. Await it to send the
393    /// request.
394    pub struct WaitForTransactionQuery {
395        client: GraphQLClient,
396        digest: TransactionDigest,
397        wait_for: WaitForTransaction,
398        timeout: Option<Duration>,
399    }
400    output: GraphQLResult<()>;
401}
402
403impl WaitForTransactionQuery {
404    /// Set how long to wait. Defaults to 60s.
405    pub fn timeout(mut self, timeout: Duration) -> Self {
406        self.timeout = Some(timeout);
407        self
408    }
409
410    async fn send(self) -> GraphQLResult<()> {
411        let client = &self.client;
412        let digest = self.digest;
413        crate::wait::timeout(
414            self.timeout.unwrap_or_else(|| Duration::from_secs(60)),
415            async {
416                loop {
417                    if match self.wait_for {
418                        WaitForTransaction::IndexedOnNode => client.is_transaction_indexed_on_node(digest).await?,
419                        WaitForTransaction::Finalized => client.is_transaction_finalized(digest).await?,
420                        _ => unimplemented!(
421                            "a new WaitForTransaction enum variant was added and needs to be handled"
422                        ),
423                    } {
424                        break Ok(());
425                    }
426                    crate::wait::sleep(Duration::from_millis(100)).await;
427                }
428            },
429        )
430        .await
431        .map_err(|_| GraphQLError::Timeout)?
432    }
433}
434
435define_query! {
436    /// Query for [`GraphQLClient::transaction`]. Await it to send the request.
437    pub struct GetTransactionQuery {
438        client: GraphQLClient,
439        digest: TransactionDigest,
440    }
441    output: GraphQLResult<Option<SignedTransaction>>;
442}
443
444impl GetTransactionQuery {
445    async fn send(self) -> GraphQLResult<Option<SignedTransaction>> {
446        let operation = TransactionBlockQueryFragment::build(TransactionBlockArgs {
447            digest: self.digest.to_string(),
448        });
449        let response = self.client.run_query(&operation).await?;
450
451        response
452            .transaction_block
453            .map(TryInto::try_into)
454            .transpose()
455    }
456}
457
458define_query! {
459    /// Query for [`GraphQLClient::transaction_effects`]. Await it to send the
460    /// request.
461    pub struct GetTransactionEffectsQuery {
462        client: GraphQLClient,
463        digest: TransactionDigest,
464    }
465    output: GraphQLResult<Option<TransactionEffects>>;
466}
467
468impl GetTransactionEffectsQuery {
469    async fn send(self) -> GraphQLResult<Option<TransactionEffects>> {
470        let operation = TransactionBlockEffectsQueryFragment::build(TransactionBlockArgs {
471            digest: self.digest.to_string(),
472        });
473        let response = self.client.run_query(&operation).await?;
474
475        response
476            .transaction_block
477            .map(TryInto::try_into)
478            .transpose()
479    }
480}
481
482define_query! {
483    /// Query for [`GraphQLClient::transaction_data_effects`]. Await it to send
484    /// the request.
485    pub struct GetTransactionDataEffectsQuery {
486        client: GraphQLClient,
487        digest: TransactionDigest,
488    }
489    output: GraphQLResult<Option<TransactionDataEffects>>;
490}
491
492impl GetTransactionDataEffectsQuery {
493    async fn send(self) -> GraphQLResult<Option<TransactionDataEffects>> {
494        let operation = TransactionBlockWithEffectsQueryFragment::build(TransactionBlockArgs {
495            digest: self.digest.to_string(),
496        });
497        let response = self.client.run_query(&operation).await?;
498
499        match response.transaction_block.map(|tx| (tx.bcs, tx.effects)) {
500            Some((Some(bcs), Some(effects))) => {
501                let bcs = crate::base64::decode(bcs.0.as_str())?;
502                let effects = crate::base64::decode(effects.bcs.unwrap().0.as_str())?;
503                let transaction: SenderSignedTransaction =
504                    bcs::from_bytes(&bcs).map_err(iota_types::BcsError::new)?;
505                let effects: TransactionEffects =
506                    bcs::from_bytes(&effects).map_err(iota_types::BcsError::new)?;
507
508                Ok(Some(TransactionDataEffects {
509                    signed_transaction: transaction.into(),
510                    effects,
511                }))
512            }
513            _ => Ok(None),
514        }
515    }
516}
517
518define_query! {
519    /// Query for [`GraphQLClient::is_transaction_indexed_on_node`]. Await it to
520    /// send the request.
521    pub struct IsTransactionIndexedOnNodeQuery {
522        client: GraphQLClient,
523        digest: TransactionDigest,
524    }
525    output: GraphQLResult<bool>;
526}
527
528impl IsTransactionIndexedOnNodeQuery {
529    async fn send(self) -> GraphQLResult<bool> {
530        let operation = TransactionBlockIndexedQueryFragment::build(TransactionBlockArgs {
531            digest: self.digest.to_string(),
532        });
533        Ok(self
534            .client
535            .run_query(&operation)
536            .await?
537            .is_transaction_indexed_on_node)
538    }
539}
540
541define_query! {
542    /// Query for [`GraphQLClient::is_transaction_finalized`]. Await it to send
543    /// the request.
544    pub struct IsTransactionFinalizedQuery {
545        client: GraphQLClient,
546        digest: TransactionDigest,
547    }
548    output: GraphQLResult<bool>;
549}
550
551impl IsTransactionFinalizedQuery {
552    async fn send(self) -> GraphQLResult<bool> {
553        let operation = TransactionBlockCheckpointQueryFragment::build(TransactionBlockArgs {
554            digest: self.digest.to_string(),
555        });
556        let response = self.client.run_query(&operation).await?;
557        if let Some(block) = response.transaction_block
558            && block
559                .effects
560                .as_ref()
561                .and_then(|e| e.checkpoint.as_ref())
562                .is_some()
563        {
564            return Ok(true);
565        }
566        Ok(false)
567    }
568}
569
570impl GraphQLClient {
571    /// Get a transaction by its digest.
572    pub fn transaction(&self, digest: TransactionDigest) -> GetTransactionQuery {
573        GetTransactionQuery {
574            client: self.clone(),
575            digest,
576        }
577    }
578
579    /// Get a page of transactions.
580    pub fn transactions(&self) -> ListTransactionsQuery {
581        ListTransactionsQuery {
582            client: self.clone(),
583            filter: None,
584            pagination: PaginationFilter::default(),
585        }
586    }
587
588    /// Get a page of transactions related to the given address.
589    pub fn address_transactions(&self, address: Address) -> ListAddressTransactionsQuery {
590        ListAddressTransactionsQuery {
591            client: self.clone(),
592            address,
593            relation: None,
594            filter: None,
595            pagination: PaginationFilter::default(),
596        }
597    }
598
599    /// Get a transaction's effects by its digest.
600    pub fn transaction_effects(&self, digest: TransactionDigest) -> GetTransactionEffectsQuery {
601        GetTransactionEffectsQuery {
602            client: self.clone(),
603            digest,
604        }
605    }
606
607    /// Get a page of transactions' effects.
608    pub fn transactions_effects(&self) -> ListTransactionsEffectsQuery {
609        ListTransactionsEffectsQuery {
610            client: self.clone(),
611            filter: None,
612            pagination: PaginationFilter::default(),
613        }
614    }
615
616    /// Get a transaction's data and effects by its digest.
617    pub fn transaction_data_effects(
618        &self,
619        digest: TransactionDigest,
620    ) -> GetTransactionDataEffectsQuery {
621        GetTransactionDataEffectsQuery {
622            client: self.clone(),
623            digest,
624        }
625    }
626
627    /// Get a page of transactions' data and effects.
628    pub fn transactions_data_effects(&self) -> ListTransactionsDataEffectsQuery {
629        ListTransactionsDataEffectsQuery {
630            client: self.clone(),
631            filter: None,
632            pagination: PaginationFilter::default(),
633        }
634    }
635
636    /// Execute a transaction.
637    pub fn execute_transaction(
638        &self,
639        signatures: &[UserSignature],
640        transaction: &Transaction,
641    ) -> ExecuteTransactionQuery {
642        ExecuteTransactionQuery {
643            client: self.clone(),
644            signatures: signatures.iter().map(|s| s.to_base64()).collect(),
645            transaction: transaction.clone(),
646            wait_for: None,
647        }
648    }
649
650    /// Returns whether the transaction for the given digest has been indexed
651    /// on the node. This means that it can be queried by its digest and its
652    /// effects will be usable for subsequent transactions. To check for
653    /// full finalization, use [`Self::is_transaction_finalized`].
654    pub fn is_transaction_indexed_on_node(
655        &self,
656        digest: TransactionDigest,
657    ) -> IsTransactionIndexedOnNodeQuery {
658        IsTransactionIndexedOnNodeQuery {
659            client: self.clone(),
660            digest,
661        }
662    }
663
664    /// Returns whether the transaction for the given digest has been included
665    /// in a checkpoint (finalized).
666    pub fn is_transaction_finalized(
667        &self,
668        digest: TransactionDigest,
669    ) -> IsTransactionFinalizedQuery {
670        IsTransactionFinalizedQuery {
671            client: self.clone(),
672            digest,
673        }
674    }
675
676    /// Wait for the indexing or finalization of a transaction by its digest.
677    /// Resolves to an error if it takes longer than the timeout, 60s unless
678    /// set with [`timeout`](WaitForTransactionQuery::timeout).
679    pub fn wait_for_transaction(
680        &self,
681        digest: TransactionDigest,
682        wait_for: WaitForTransaction,
683    ) -> WaitForTransactionQuery {
684        WaitForTransactionQuery {
685            client: self.clone(),
686            digest,
687            wait_for,
688            timeout: None,
689        }
690    }
691}
692
693#[cfg(all(test, not(target_arch = "wasm32")))]
694mod tests {
695    use std::time::Duration;
696
697    use base64ct::Encoding;
698    use iota_types::{Address, Ed25519PublicKey, Ed25519Signature, SimpleSignature, UserSignature};
699
700    use crate::{
701        GraphQLClient, GraphQLError, WaitForTransaction,
702        query_types::{AddressTransactionRelationship, TransactionsFilter},
703        test_utils::{
704            assert_backward_page, assert_forward_page, backward_page, forward_page, sent_variables,
705            test_client, test_transaction,
706        },
707    };
708
709    #[tokio::test]
710    async fn digest_getters_send_the_digest() {
711        let digest = test_transaction().digest();
712        let expected = digest.to_string();
713        let vars = sent_variables("TransactionBlockQueryFragment", |client| async move {
714            let _ = client.transaction(digest).await;
715        })
716        .await;
717        assert_eq!(vars["digest"], expected);
718
719        let vars = sent_variables(
720            "TransactionBlockEffectsQueryFragment",
721            |client| async move {
722                let _ = client.transaction_effects(digest).await;
723            },
724        )
725        .await;
726        assert_eq!(vars["digest"], expected);
727
728        let vars = sent_variables(
729            "TransactionBlockWithEffectsQueryFragment",
730            |client| async move {
731                let _ = client.transaction_data_effects(digest).await;
732            },
733        )
734        .await;
735        assert_eq!(vars["digest"], expected);
736
737        let vars = sent_variables(
738            "TransactionBlockIndexedQueryFragment",
739            |client| async move {
740                let _ = client.is_transaction_indexed_on_node(digest).await;
741            },
742        )
743        .await;
744        assert_eq!(vars["digest"], expected);
745
746        let vars = sent_variables(
747            "TransactionBlockCheckpointQueryFragment",
748            |client| async move {
749                let _ = client.is_transaction_finalized(digest).await;
750            },
751        )
752        .await;
753        assert_eq!(vars["digest"], expected);
754    }
755
756    #[tokio::test]
757    async fn execute_transaction_sends_the_signatures_and_transaction() {
758        let transaction = test_transaction();
759        let signature = UserSignature::Simple(SimpleSignature::Ed25519 {
760            signature: Ed25519Signature::new([1; 64]),
761            public_key: Ed25519PublicKey::new([2; 32]),
762        });
763        let expected_signature = signature.to_base64();
764        let expected_tx_bytes =
765            base64ct::Base64::encode_string(&bcs::to_bytes(&transaction).unwrap());
766        let vars = sent_variables("ExecuteTransactionQueryFragment", |client| async move {
767            let _ = client.execute_transaction(&[signature], &transaction).await;
768        })
769        .await;
770        assert_eq!(vars["signatures"], serde_json::json!([expected_signature]));
771        assert_eq!(vars["txBytes"], expected_tx_bytes);
772    }
773
774    #[tokio::test]
775    async fn wait_for_transaction_sends_the_digest_to_the_status_query() {
776        let digest = test_transaction().digest();
777        for (wait_for, operation) in [
778            (
779                WaitForTransaction::IndexedOnNode,
780                "TransactionBlockIndexedQueryFragment",
781            ),
782            (
783                WaitForTransaction::Finalized,
784                "TransactionBlockCheckpointQueryFragment",
785            ),
786        ] {
787            let vars = sent_variables(operation, |client| async move {
788                let _ = client.wait_for_transaction(digest, wait_for).await;
789            })
790            .await;
791            assert_eq!(vars["digest"], digest.to_string());
792        }
793    }
794
795    #[tokio::test]
796    async fn wait_for_transaction_stops_at_the_timeout() {
797        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
798        let client =
799            GraphQLClient::new(&format!("http://{}", listener.local_addr().unwrap())).unwrap();
800        let result = tokio::time::timeout(
801            Duration::from_secs(10),
802            client
803                .wait_for_transaction(test_transaction().digest(), WaitForTransaction::Finalized)
804                .timeout(Duration::from_millis(100)),
805        )
806        .await
807        .expect("the query's own timeout fires first");
808        assert!(matches!(result, Err(GraphQLError::Timeout)));
809    }
810
811    fn sent_by_framework() -> TransactionsFilter {
812        TransactionsFilter::default().with_sent_address(Address::FRAMEWORK)
813    }
814
815    #[tokio::test]
816    async fn transactions_sends_the_filter_and_pagination() {
817        let vars = sent_variables("TransactionBlocksQueryFragment", |client| async move {
818            let _ = client
819                .transactions()
820                .filter(sent_by_framework())
821                .pagination(backward_page())
822                .await;
823        })
824        .await;
825        assert_eq!(
826            vars["filter"]["sentAddress"],
827            Address::FRAMEWORK.to_string()
828        );
829        assert_backward_page(&vars);
830
831        let vars = sent_variables("TransactionBlocksQueryFragment", |client| async move {
832            let _ = client
833                .transactions()
834                .filter(sent_by_framework())
835                .pagination(forward_page())
836                .await;
837        })
838        .await;
839        assert_forward_page(&vars);
840    }
841
842    #[tokio::test]
843    async fn transactions_effects_sends_the_filter_and_pagination() {
844        let vars = sent_variables(
845            "TransactionBlocksEffectsQueryFragment",
846            |client| async move {
847                let _ = client
848                    .transactions_effects()
849                    .filter(sent_by_framework())
850                    .pagination(backward_page())
851                    .await;
852            },
853        )
854        .await;
855        assert_eq!(
856            vars["filter"]["sentAddress"],
857            Address::FRAMEWORK.to_string()
858        );
859        assert_backward_page(&vars);
860
861        let vars = sent_variables(
862            "TransactionBlocksEffectsQueryFragment",
863            |client| async move {
864                let _ = client
865                    .transactions_effects()
866                    .filter(sent_by_framework())
867                    .pagination(forward_page())
868                    .await;
869            },
870        )
871        .await;
872        assert_forward_page(&vars);
873    }
874
875    #[tokio::test]
876    async fn transactions_data_effects_sends_the_filter_and_pagination() {
877        let vars = sent_variables(
878            "TransactionBlocksWithEffectsQueryFragment",
879            |client| async move {
880                let _ = client
881                    .transactions_data_effects()
882                    .filter(sent_by_framework())
883                    .pagination(backward_page())
884                    .await;
885            },
886        )
887        .await;
888        assert_eq!(
889            vars["filter"]["sentAddress"],
890            Address::FRAMEWORK.to_string()
891        );
892        assert_backward_page(&vars);
893
894        let vars = sent_variables(
895            "TransactionBlocksWithEffectsQueryFragment",
896            |client| async move {
897                let _ = client
898                    .transactions_data_effects()
899                    .filter(sent_by_framework())
900                    .pagination(forward_page())
901                    .await;
902            },
903        )
904        .await;
905        assert_forward_page(&vars);
906    }
907
908    #[tokio::test]
909    async fn address_transactions_sends_the_address_relation_filter_and_pagination() {
910        let vars = sent_variables("AddressTransactionsQueryFragment", |client| async move {
911            let _ = client
912                .address_transactions(Address::STD)
913                .relation(AddressTransactionRelationship::Recv)
914                .filter(sent_by_framework())
915                .pagination(backward_page())
916                .await;
917        })
918        .await;
919        assert_eq!(vars["address"], Address::STD.to_string());
920        assert_eq!(vars["relation"], "RECV");
921        assert_eq!(
922            vars["filter"]["sentAddress"],
923            Address::FRAMEWORK.to_string()
924        );
925        assert_backward_page(&vars);
926
927        let vars = sent_variables("AddressTransactionsQueryFragment", |client| async move {
928            let _ = client
929                .address_transactions(Address::STD)
930                .relation(AddressTransactionRelationship::Recv)
931                .filter(sent_by_framework())
932                .pagination(forward_page())
933                .await;
934        })
935        .await;
936        assert_forward_page(&vars);
937    }
938
939    #[tokio::test]
940    async fn test_transaction_effects_query() {
941        let client = test_client();
942        let transactions = client.transactions().await.unwrap();
943        let tx_digest = transactions.data()[0].transaction.digest();
944        let effects = client.transaction_effects(tx_digest).await.unwrap();
945        assert!(
946            effects.is_some(),
947            "Transaction effects query failed for {} network.",
948            client.rpc_server(),
949        );
950    }
951
952    #[tokio::test]
953    async fn test_transactions_effects_query() {
954        let client = test_client();
955        client
956            .transactions_effects()
957            .await
958            .map_err(|e| {
959                format!(
960                    "Transactions effects query failed for {} network: Error {e}",
961                    client.rpc_server()
962                )
963            })
964            .unwrap();
965    }
966
967    #[tokio::test]
968    async fn test_transactions_query() {
969        let client = test_client();
970        let transactions = client
971            .transactions()
972            .await
973            .map_err(|e| {
974                format!(
975                    "Transactions query failed for {} network: Error {e}",
976                    client.rpc_server()
977                )
978            })
979            .unwrap();
980        assert!(
981            !transactions.is_empty(),
982            "Transactions query returned no data for {} network",
983            client.rpc_server()
984        );
985    }
986
987    #[tokio::test]
988    async fn test_address_transactions() {
989        let client = test_client();
990        let transactions = client.transactions().await.unwrap();
991        let sender = transactions.data()[0].transaction.as_v1().sender;
992
993        for relation in [
994            AddressTransactionRelationship::Sent,
995            AddressTransactionRelationship::Recv,
996            AddressTransactionRelationship::Affected,
997        ] {
998            let page = client
999                .address_transactions(sender)
1000                .relation(relation)
1001                .await
1002                .map_err(|e| {
1003                    format!(
1004                        "Address transactions query with relation {relation:?} failed for {} \
1005                         network: Error {e}",
1006                        client.rpc_server()
1007                    )
1008                })
1009                .unwrap();
1010
1011            if matches!(relation, AddressTransactionRelationship::Sent) {
1012                assert!(
1013                    page.data()
1014                        .iter()
1015                        .all(|tx| tx.transaction.as_v1().sender == sender),
1016                    "Sent relation returned a transaction from another sender"
1017                );
1018            }
1019        }
1020    }
1021
1022    #[tokio::test]
1023    async fn test_transaction_data_effects() {
1024        let client = test_client();
1025        let transactions = client.transactions().await.unwrap();
1026        let digest = transactions.data()[0].transaction.digest();
1027
1028        client
1029            .transaction_data_effects(digest)
1030            .await
1031            .unwrap()
1032            .unwrap();
1033    }
1034
1035    #[tokio::test]
1036    async fn test_transactions_data_effects() {
1037        let client = test_client();
1038        let transactions = client.transactions().await.unwrap();
1039        let digest = transactions.data()[0].transaction.digest();
1040
1041        client
1042            .transactions_data_effects()
1043            .filter(TransactionsFilter::default().with_transaction_ids([digest]))
1044            .await
1045            .unwrap();
1046    }
1047}