Skip to main content

iota_sdk_transaction_builder/builder/
client.rs

1// Copyright (c) 2025 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4use std::collections::BTreeMap;
5
6use iota_types::{
7    Address, Object, ObjectId, StructTag, Transaction, TransactionDigest, TransactionEffects,
8    UserSignature, Version,
9};
10
11/// Determines what to wait for after executing a transaction.
12#[derive(Default)]
13#[non_exhaustive]
14pub enum WaitForTransaction {
15    /// Indicates that the transaction effects will be usable in subsequent
16    /// transactions (you can reference objects created by this transaction),
17    /// and that the transaction itself is indexed on the fullnode, so queries
18    /// served by the fullnode, such as gRPC, will find it.
19    ///
20    /// **Warning:** This does not guarantee the transaction is indexed on the
21    /// indexer, so queries served by an indexer, such as GraphQL, may not
22    /// find it yet. Use [`WaitForTransaction::Finalized`] with those clients.
23    IndexedOnNode,
24    /// Indicates that the transaction has been included in a checkpoint, and
25    /// all queries may include it.
26    #[default]
27    Finalized,
28}
29
30/// One page of objects plus an optional cursor for the next page. See
31/// [`TransactionBuilderLedgerClient::objects`].
32#[derive(Clone, Debug)]
33pub struct ObjectsPage {
34    /// The objects in this page.
35    pub data: Vec<Object>,
36    /// Opaque continuation cursor for fetching the next page; `None` when no
37    /// further pages exist. Pass it back as the `cursor` argument to
38    /// [`TransactionBuilderLedgerClient::objects`] to advance.
39    pub next_cursor: Option<Vec<u8>>,
40}
41
42/// Transport-neutral view of the chain's protocol configuration: a flat
43/// map of attribute name to value, parsed by callers as needed.
44#[derive(Clone, Debug, Default)]
45pub struct ProtocolConfig {
46    pub(crate) attributes: BTreeMap<String, String>,
47}
48
49impl ProtocolConfig {
50    /// Builds a config from attributes keyed by their canonical protocol name
51    /// (e.g. `"max_gas_payment_objects"`).
52    pub fn new(attributes: BTreeMap<String, String>) -> Self {
53        Self { attributes }
54    }
55
56    /// All available configuration attributes, keyed by their canonical
57    /// protocol name (e.g. `"max_gas_payment_objects"`).
58    pub fn attributes(&self) -> &BTreeMap<String, String> {
59        &self.attributes
60    }
61
62    /// Looks up one attribute by its canonical protocol name.
63    pub fn attribute(&self, name: &str) -> Option<&str> {
64        self.attributes.get(name).map(String::as_str)
65    }
66}
67
68/// Base trait shared by the transaction builder client traits, carrying the
69/// client's error type.
70pub trait TransactionBuilderClientBase {
71    /// The error type for this client.
72    type Error: 'static + std::error::Error + Send + Sync;
73}
74
75/// Read-only access to ledger state: everything the Transaction Builder needs
76/// to resolve and build a transaction
77/// ([`finish_with_budget`](crate::TransactionBuilder::finish_with_budget)).
78pub trait TransactionBuilderLedgerClient: TransactionBuilderClientBase {
79    /// Fetch an object
80    fn object(
81        &self,
82        object_id: ObjectId,
83        version: impl Into<Option<Version>>,
84    ) -> impl std::future::Future<Output = Result<Option<Object>, Self::Error>>;
85
86    /// Fetch several objects at once, returning them in the order they were
87    /// requested with `None` in place of any object that does not exist.
88    ///
89    /// The default impl calls [`object`](Self::object) once per entry, costing
90    /// one round trip each. Clients whose transport can fetch a batch (such as
91    /// gRPC's `GetObjects`) should override it.
92    fn objects_by_id(
93        &self,
94        object_ids: &[(ObjectId, Option<Version>)],
95    ) -> impl std::future::Future<Output = Result<Vec<Option<Object>>, Self::Error>> {
96        async move {
97            let mut objects = Vec::with_capacity(object_ids.len());
98            for (object_id, version) in object_ids {
99                objects.push(self.object(*object_id, *version).await?);
100            }
101            Ok(objects)
102        }
103    }
104
105    /// Fetch one page of objects matching the filter, returning the page
106    /// contents and a continuation cursor (when more pages exist).
107    ///
108    /// The cursor is opaque to callers — both GraphQL (base64-encoded
109    /// JSON/BCS) and gRPC (`prost::bytes::Bytes` page token) formats fit
110    /// into `Option<Vec<u8>>`. Pass `None` to start from the beginning;
111    /// pass the cursor returned by a previous call to advance.
112    fn objects(
113        &self,
114        struct_tag: Option<StructTag>,
115        owner: Address,
116        cursor: Option<Vec<u8>>,
117        limit: Option<usize>,
118    ) -> impl std::future::Future<Output = Result<ObjectsPage, Self::Error>>;
119
120    /// Fetch the chain's protocol configuration.
121    fn protocol_config(
122        &self,
123    ) -> impl std::future::Future<Output = Result<ProtocolConfig, Self::Error>>;
124
125    /// Get the reference gas price
126    fn reference_gas_price(
127        &self,
128        epoch: impl Into<Option<u64>>,
129    ) -> impl std::future::Future<Output = Result<Option<u64>, Self::Error>>;
130}
131
132/// Transaction simulation: dry runs and the gas budget estimation built on
133/// them ([`finish`](crate::TransactionBuilder::finish),
134/// [`dry_run`](crate::TransactionBuilder::dry_run)).
135pub trait TransactionBuilderSimulationClient: TransactionBuilderClientBase {
136    /// The result of a dry run.
137    type DryRunResult;
138
139    /// Estimate the gas budget needed for a transaction, typically by
140    /// simulating it and reading the gas cost from the result. `Ok(None)`
141    /// means no estimate is available;
142    /// [`finish`](crate::TransactionBuilder::finish) then fails unless a
143    /// budget was set explicitly.
144    fn estimate_transaction_budget(
145        &self,
146        transaction: &Transaction,
147    ) -> impl std::future::Future<Output = Result<Option<u64>, Self::Error>>;
148
149    /// Dry run a transaction
150    fn dry_run_transaction(
151        &self,
152        transaction: &Transaction,
153        skip_checks: bool,
154    ) -> impl std::future::Future<Output = Result<Self::DryRunResult, Self::Error>>;
155}
156
157/// Transaction execution: submitting a transaction and tracking its result
158/// ([`execute`](crate::TransactionBuilder::execute)).
159pub trait TransactionBuilderExecutionClient: TransactionBuilderClientBase {
160    /// Execute a transaction
161    ///
162    /// If the wait fails, the error is returned although the transaction was
163    /// executed, and its effects are lost. To keep them, pass `None` and call
164    /// [`wait_for_transaction`](Self::wait_for_transaction) afterwards.
165    fn execute_transaction(
166        &self,
167        signatures: &[UserSignature],
168        transaction: &Transaction,
169        wait_for: impl Into<Option<WaitForTransaction>>,
170    ) -> impl std::future::Future<Output = Result<TransactionEffects, Self::Error>>;
171
172    /// Wait for the indexing or finalization of a transaction by its digest.
173    fn wait_for_transaction(
174        &self,
175        digest: TransactionDigest,
176        wait_for: WaitForTransaction,
177    ) -> impl std::future::Future<Output = Result<(), Self::Error>>;
178
179    /// Fetch the effects of an executed transaction
180    fn transaction_effects(
181        &self,
182        digest: TransactionDigest,
183    ) -> impl std::future::Future<Output = Result<Option<TransactionEffects>, Self::Error>>;
184}
185
186/// A full transaction builder client: ledger reads, simulation, and execution.
187///
188/// This is a blanket alias — do not implement it directly. Implement
189/// [`TransactionBuilderLedgerClient`], [`TransactionBuilderSimulationClient`],
190/// and [`TransactionBuilderExecutionClient`] instead, and this trait is
191/// implemented automatically.
192pub trait TransactionBuilderClient:
193    TransactionBuilderLedgerClient
194    + TransactionBuilderSimulationClient
195    + TransactionBuilderExecutionClient
196{
197}
198
199impl<T> TransactionBuilderClient for T where
200    T: TransactionBuilderLedgerClient
201        + TransactionBuilderSimulationClient
202        + TransactionBuilderExecutionClient
203{
204}
205
206impl<T: TransactionBuilderClientBase> TransactionBuilderClientBase for &T {
207    type Error = T::Error;
208}
209
210impl<T: TransactionBuilderLedgerClient> TransactionBuilderLedgerClient for &T {
211    fn object(
212        &self,
213        object_id: ObjectId,
214        version: impl Into<Option<Version>>,
215    ) -> impl std::future::Future<Output = Result<Option<Object>, Self::Error>> {
216        (*self).object(object_id, version)
217    }
218
219    fn objects_by_id(
220        &self,
221        object_ids: &[(ObjectId, Option<Version>)],
222    ) -> impl std::future::Future<Output = Result<Vec<Option<Object>>, Self::Error>> {
223        (*self).objects_by_id(object_ids)
224    }
225
226    fn objects(
227        &self,
228        struct_tag: Option<StructTag>,
229        owner: Address,
230        cursor: Option<Vec<u8>>,
231        limit: Option<usize>,
232    ) -> impl std::future::Future<Output = Result<ObjectsPage, Self::Error>> {
233        (*self).objects(struct_tag, owner, cursor, limit)
234    }
235
236    fn protocol_config(
237        &self,
238    ) -> impl std::future::Future<Output = Result<ProtocolConfig, Self::Error>> {
239        (*self).protocol_config()
240    }
241
242    fn reference_gas_price(
243        &self,
244        epoch: impl Into<Option<u64>>,
245    ) -> impl std::future::Future<Output = Result<Option<u64>, Self::Error>> {
246        (*self).reference_gas_price(epoch)
247    }
248}
249
250impl<T: TransactionBuilderSimulationClient> TransactionBuilderSimulationClient for &T {
251    type DryRunResult = T::DryRunResult;
252
253    fn estimate_transaction_budget(
254        &self,
255        transaction: &Transaction,
256    ) -> impl std::future::Future<Output = Result<Option<u64>, Self::Error>> {
257        (*self).estimate_transaction_budget(transaction)
258    }
259
260    fn dry_run_transaction(
261        &self,
262        transaction: &Transaction,
263        skip_checks: bool,
264    ) -> impl std::future::Future<Output = Result<Self::DryRunResult, Self::Error>> {
265        (*self).dry_run_transaction(transaction, skip_checks)
266    }
267}
268
269impl<T: TransactionBuilderExecutionClient> TransactionBuilderExecutionClient for &T {
270    fn execute_transaction(
271        &self,
272        signatures: &[UserSignature],
273        transaction: &Transaction,
274        wait_for: impl Into<Option<WaitForTransaction>>,
275    ) -> impl std::future::Future<Output = Result<TransactionEffects, Self::Error>> {
276        (*self).execute_transaction(signatures, transaction, wait_for)
277    }
278
279    fn wait_for_transaction(
280        &self,
281        digest: TransactionDigest,
282        wait_for: WaitForTransaction,
283    ) -> impl std::future::Future<Output = Result<(), Self::Error>> {
284        (*self).wait_for_transaction(digest, wait_for)
285    }
286
287    fn transaction_effects(
288        &self,
289        digest: TransactionDigest,
290    ) -> impl std::future::Future<Output = Result<Option<TransactionEffects>, Self::Error>> {
291        (*self).transaction_effects(digest)
292    }
293}
294
295impl<T: TransactionBuilderClientBase> TransactionBuilderClientBase for std::sync::Arc<T> {
296    type Error = T::Error;
297}
298
299impl<T: TransactionBuilderLedgerClient> TransactionBuilderLedgerClient for std::sync::Arc<T> {
300    fn object(
301        &self,
302        object_id: ObjectId,
303        version: impl Into<Option<Version>>,
304    ) -> impl std::future::Future<Output = Result<Option<Object>, Self::Error>> {
305        self.as_ref().object(object_id, version)
306    }
307
308    fn objects_by_id(
309        &self,
310        object_ids: &[(ObjectId, Option<Version>)],
311    ) -> impl std::future::Future<Output = Result<Vec<Option<Object>>, Self::Error>> {
312        self.as_ref().objects_by_id(object_ids)
313    }
314
315    fn objects(
316        &self,
317        struct_tag: Option<StructTag>,
318        owner: Address,
319        cursor: Option<Vec<u8>>,
320        limit: Option<usize>,
321    ) -> impl std::future::Future<Output = Result<ObjectsPage, Self::Error>> {
322        self.as_ref().objects(struct_tag, owner, cursor, limit)
323    }
324
325    fn protocol_config(
326        &self,
327    ) -> impl std::future::Future<Output = Result<ProtocolConfig, Self::Error>> {
328        self.as_ref().protocol_config()
329    }
330
331    fn reference_gas_price(
332        &self,
333        epoch: impl Into<Option<u64>>,
334    ) -> impl std::future::Future<Output = Result<Option<u64>, Self::Error>> {
335        self.as_ref().reference_gas_price(epoch)
336    }
337}
338
339impl<T: TransactionBuilderSimulationClient> TransactionBuilderSimulationClient
340    for std::sync::Arc<T>
341{
342    type DryRunResult = T::DryRunResult;
343
344    fn estimate_transaction_budget(
345        &self,
346        transaction: &Transaction,
347    ) -> impl std::future::Future<Output = Result<Option<u64>, Self::Error>> {
348        self.as_ref().estimate_transaction_budget(transaction)
349    }
350
351    fn dry_run_transaction(
352        &self,
353        transaction: &Transaction,
354        skip_checks: bool,
355    ) -> impl std::future::Future<Output = Result<Self::DryRunResult, Self::Error>> {
356        self.as_ref().dry_run_transaction(transaction, skip_checks)
357    }
358}
359
360impl<T: TransactionBuilderExecutionClient> TransactionBuilderExecutionClient for std::sync::Arc<T> {
361    fn execute_transaction(
362        &self,
363        signatures: &[UserSignature],
364        transaction: &Transaction,
365        wait_for: impl Into<Option<WaitForTransaction>>,
366    ) -> impl std::future::Future<Output = Result<TransactionEffects, Self::Error>> {
367        self.as_ref()
368            .execute_transaction(signatures, transaction, wait_for)
369    }
370
371    fn wait_for_transaction(
372        &self,
373        digest: TransactionDigest,
374        wait_for: WaitForTransaction,
375    ) -> impl std::future::Future<Output = Result<(), Self::Error>> {
376        self.as_ref().wait_for_transaction(digest, wait_for)
377    }
378
379    fn transaction_effects(
380        &self,
381        digest: TransactionDigest,
382    ) -> impl std::future::Future<Output = Result<Option<TransactionEffects>, Self::Error>> {
383        self.as_ref().transaction_effects(digest)
384    }
385}
386
387#[cfg(feature = "test-client")]
388pub(crate) mod test_client {
389    //! Test utilities for the transaction builder.
390
391    use iota_types::{
392        Address, MoveStruct, Object, ObjectData, ObjectId, Owner, StructTag, Transaction,
393        TransactionDigest, TransactionEffects, UserSignature, Version,
394    };
395
396    use super::{
397        TransactionBuilderClientBase, TransactionBuilderExecutionClient,
398        TransactionBuilderLedgerClient, TransactionBuilderSimulationClient, WaitForTransaction,
399    };
400    use crate::{
401        ObjectsPage,
402        builder::{BASE_TX_COST_FIXED_KEY, MAX_GAS_PAYMENT_OBJECTS_KEY},
403    };
404
405    /// Balance, in NANOS, of every fabricated coin. Large enough to cover any
406    /// gas budget the builder might estimate in a doc test or example.
407    const FABRICATED_COIN_BALANCE: u64 = 1_000_000_000_000;
408
409    /// Build a fabricated gas coin (`0x2::coin::Coin<0x2::iota::IOTA>`) with
410    /// the given id, owner and balance.
411    ///
412    /// The contents are the BCS layout the coin resolution code expects: the
413    /// 32-byte object id followed by the little-endian `u64` balance.
414    fn fabricated_coin(object_id: ObjectId, owner: Owner, balance: u64) -> Object {
415        let mut contents = Vec::with_capacity(ObjectId::LENGTH + std::mem::size_of::<u64>());
416        contents.extend_from_slice(object_id.as_ref());
417        contents.extend_from_slice(&balance.to_le_bytes());
418        let move_struct = MoveStruct::new(
419            StructTag::new_gas_coin().into(),
420            Version::from_u64(1),
421            contents,
422        )
423        .expect("contents always contain a full object id");
424        Object::new(
425            ObjectData::Struct(move_struct),
426            owner,
427            TransactionDigest::ZERO,
428            0,
429        )
430    }
431
432    /// A test client that implements the transaction builder client traits by
433    /// fabricating objects on demand.
434    ///
435    /// It is useful for building transactions in tests, examples, and doc tests
436    /// where a live network connection is not available. Object lookups resolve
437    /// to a synthesized gas coin owned by an address (shared system objects
438    /// such as the system state object resolve as shared), and gas
439    /// selection always finds a single funded coin. This is enough to drive
440    /// [`finish`](crate::TransactionBuilder::finish) to completion, but the
441    /// resulting transaction references made-up objects and cannot be executed
442    /// — [`execute_transaction`](TransactionBuilderExecutionClient::execute_transaction) returns
443    /// an error.
444    #[derive(Clone, Copy, Debug, Default)]
445    pub struct TestClient;
446
447    /// Error type for [`TestClient`].
448    #[derive(Clone, Debug, thiserror::Error)]
449    #[error("TestClientError: {0}")]
450    pub struct TestClientError(pub String);
451
452    impl TransactionBuilderClientBase for TestClient {
453        type Error = TestClientError;
454    }
455
456    impl TransactionBuilderLedgerClient for TestClient {
457        async fn object(
458            &self,
459            object_id: ObjectId,
460            _version: impl Into<Option<Version>>,
461        ) -> Result<Option<Object>, Self::Error> {
462            // System objects (e.g. the system state object used by staking) are
463            // shared; everything else resolves as an address-owned coin.
464            let owner = if object_id == ObjectId::SYSTEM_STATE || object_id == ObjectId::CLOCK {
465                Owner::Shared(Version::from_u64(1))
466            } else {
467                Owner::Address(Address::ZERO)
468            };
469            Ok(Some(fabricated_coin(
470                object_id,
471                owner,
472                FABRICATED_COIN_BALANCE,
473            )))
474        }
475
476        async fn objects(
477            &self,
478            _struct_tag: Option<StructTag>,
479            owner: Address,
480            _cursor: Option<Vec<u8>>,
481            _limit: Option<usize>,
482        ) -> Result<ObjectsPage, Self::Error> {
483            // A single funded gas coin owned by the requested owner is enough for
484            // the builder's automatic gas selection. Its id is a fixed sentinel
485            // that won't collide with the object ids used in examples.
486            let gas_coin_id = ObjectId::from_bytes([0xee; ObjectId::LENGTH])
487                .expect("32 bytes is a valid object id");
488            let owner = Owner::Address(owner);
489            Ok(ObjectsPage {
490                data: vec![fabricated_coin(gas_coin_id, owner, FABRICATED_COIN_BALANCE)],
491                next_cursor: None,
492            })
493        }
494
495        async fn reference_gas_price(
496            &self,
497            _epoch: impl Into<Option<u64>>,
498        ) -> Result<Option<u64>, Self::Error> {
499            Ok(Some(1000))
500        }
501
502        async fn protocol_config(&self) -> Result<super::ProtocolConfig, Self::Error> {
503            let mut config = super::ProtocolConfig::default();
504            config
505                .attributes
506                .insert(BASE_TX_COST_FIXED_KEY.to_owned(), "1000".to_owned());
507            config
508                .attributes
509                .insert(MAX_GAS_PAYMENT_OBJECTS_KEY.to_owned(), "256".to_owned());
510            Ok(config)
511        }
512    }
513
514    impl TransactionBuilderSimulationClient for TestClient {
515        type DryRunResult = ();
516
517        async fn estimate_transaction_budget(
518            &self,
519            _transaction: &Transaction,
520        ) -> Result<Option<u64>, Self::Error> {
521            Ok(Some(50_000_000))
522        }
523
524        async fn dry_run_transaction(
525            &self,
526            _transaction: &Transaction,
527            _skip_checks: bool,
528        ) -> Result<Self::DryRunResult, Self::Error> {
529            Ok(())
530        }
531    }
532
533    impl TransactionBuilderExecutionClient for TestClient {
534        async fn execute_transaction(
535            &self,
536            _signatures: &[UserSignature],
537            _transaction: &Transaction,
538            _wait_for: impl Into<Option<WaitForTransaction>>,
539        ) -> Result<TransactionEffects, Self::Error> {
540            Err(TestClientError(
541                "TestClient cannot execute transactions".to_string(),
542            ))
543        }
544
545        async fn wait_for_transaction(
546            &self,
547            _digest: TransactionDigest,
548            _wait_for: WaitForTransaction,
549        ) -> Result<(), Self::Error> {
550            Ok(())
551        }
552
553        async fn transaction_effects(
554            &self,
555            _digest: TransactionDigest,
556        ) -> Result<Option<TransactionEffects>, Self::Error> {
557            Ok(None)
558        }
559    }
560
561    /// A [`TestClient`] that records how the builder asked for objects, and can
562    /// report chosen ids as missing.
563    #[derive(Clone, Default)]
564    pub struct RecordingClient {
565        /// The ids of each `objects_by_id` call, in call order.
566        pub batches: std::sync::Arc<std::sync::Mutex<Vec<Vec<ObjectId>>>>,
567        /// The ids of each single-object `object` call, in call order.
568        pub singles: std::sync::Arc<std::sync::Mutex<Vec<ObjectId>>>,
569        /// Ids to report as missing instead of fabricating an object.
570        pub missing: Vec<ObjectId>,
571    }
572
573    impl RecordingClient {
574        /// Returns the ids of each `objects_by_id` call, in call order.
575        pub fn batches(&self) -> Vec<Vec<ObjectId>> {
576            self.batches.lock().unwrap().clone()
577        }
578
579        /// Returns the ids of each single-object `object` call, in call order.
580        pub fn singles(&self) -> Vec<ObjectId> {
581            self.singles.lock().unwrap().clone()
582        }
583    }
584
585    impl TransactionBuilderClientBase for RecordingClient {
586        type Error = crate::TestClientError;
587    }
588
589    impl TransactionBuilderLedgerClient for RecordingClient {
590        async fn object(
591            &self,
592            object_id: ObjectId,
593            version: impl Into<Option<Version>>,
594        ) -> Result<Option<Object>, Self::Error> {
595            self.singles.lock().unwrap().push(object_id);
596            if self.missing.contains(&object_id) {
597                return Ok(None);
598            }
599            crate::TestClient.object(object_id, version).await
600        }
601
602        async fn objects_by_id(
603            &self,
604            object_ids: &[(ObjectId, Option<Version>)],
605        ) -> Result<Vec<Option<Object>>, Self::Error> {
606            self.batches
607                .lock()
608                .unwrap()
609                .push(object_ids.iter().map(|(id, _)| *id).collect());
610            let mut objects = Vec::with_capacity(object_ids.len());
611            for (object_id, _) in object_ids {
612                objects.push(if self.missing.contains(object_id) {
613                    None
614                } else {
615                    crate::TestClient.object(*object_id, None).await?
616                });
617            }
618            Ok(objects)
619        }
620
621        async fn objects(
622            &self,
623            struct_tag: Option<StructTag>,
624            owner: Address,
625            cursor: Option<Vec<u8>>,
626            limit: Option<usize>,
627        ) -> Result<crate::ObjectsPage, Self::Error> {
628            crate::TestClient
629                .objects(struct_tag, owner, cursor, limit)
630                .await
631        }
632
633        async fn reference_gas_price(
634            &self,
635            epoch: impl Into<Option<u64>>,
636        ) -> Result<Option<u64>, Self::Error> {
637            crate::TestClient.reference_gas_price(epoch).await
638        }
639
640        async fn protocol_config(&self) -> Result<super::ProtocolConfig, Self::Error> {
641            crate::TestClient.protocol_config().await
642        }
643    }
644
645    impl TransactionBuilderSimulationClient for RecordingClient {
646        type DryRunResult = ();
647
648        async fn estimate_transaction_budget(
649            &self,
650            transaction: &Transaction,
651        ) -> Result<Option<u64>, Self::Error> {
652            crate::TestClient
653                .estimate_transaction_budget(transaction)
654                .await
655        }
656
657        async fn dry_run_transaction(
658            &self,
659            transaction: &Transaction,
660            skip_checks: bool,
661        ) -> Result<Self::DryRunResult, Self::Error> {
662            crate::TestClient
663                .dry_run_transaction(transaction, skip_checks)
664                .await
665        }
666    }
667
668    impl TransactionBuilderExecutionClient for RecordingClient {
669        async fn execute_transaction(
670            &self,
671            signatures: &[iota_types::UserSignature],
672            transaction: &Transaction,
673            wait_for: impl Into<Option<WaitForTransaction>>,
674        ) -> Result<TransactionEffects, Self::Error> {
675            crate::TestClient
676                .execute_transaction(signatures, transaction, wait_for)
677                .await
678        }
679
680        async fn wait_for_transaction(
681            &self,
682            digest: iota_types::TransactionDigest,
683            wait_for: WaitForTransaction,
684        ) -> Result<(), Self::Error> {
685            crate::TestClient
686                .wait_for_transaction(digest, wait_for)
687                .await
688        }
689
690        async fn transaction_effects(
691            &self,
692            digest: iota_types::TransactionDigest,
693        ) -> Result<Option<TransactionEffects>, Self::Error> {
694            crate::TestClient.transaction_effects(digest).await
695        }
696    }
697}