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