1use crate::error::Error;
2use std::collections::BTreeMap;
3use std::collections::BTreeSet;
4use std::collections::HashMap;
5use sui_sdk_types::Address;
6use sui_sdk_types::Digest;
7use sui_sdk_types::Identifier;
8use sui_sdk_types::Transaction;
9use sui_sdk_types::TransactionExpiration;
10use sui_sdk_types::TypeTag;
11
12#[derive(Default)]
14pub struct TransactionBuilder {
15 pub(crate) gas: Vec<ObjectInput>,
19 gas_budget: Option<u64>,
21 gas_price: Option<u64>,
23 sender: Option<Address>,
25 sponsor: Option<Address>,
27 expiration: Option<TransactionExpiration>,
29
30 #[cfg(feature = "intents")]
32 pub(crate) resolvers: BTreeMap<std::any::TypeId, Box<dyn crate::intent::IntentResolver>>,
33
34 pub(crate) arguments: BTreeMap<usize, ResolvedArgument>,
35 inputs: HashMap<InputArgKind, (usize, InputArg)>,
36 pub(crate) commands: BTreeMap<usize, Command>,
37 pub(crate) intents: BTreeMap<usize, Box<dyn std::any::Any + Send + Sync>>,
38}
39
40#[derive(Clone, Copy, Debug)]
41pub(crate) enum ResolvedArgument {
42 Unresolved,
43 #[allow(unused)]
44 ReplaceWith(Argument),
45 Resolved(sui_sdk_types::Argument),
46}
47
48#[derive(Debug, PartialEq, Eq, Hash)]
49pub(crate) enum InputArgKind {
50 Gas,
51 ObjectInput(Address),
52 PureInput(Vec<u8>),
53 UniquePureInput(usize),
54}
55
56pub(crate) enum InputArg {
57 Gas,
58 Pure(Vec<u8>),
59 Object(ObjectInput),
60}
61
62impl TransactionBuilder {
63 pub fn new() -> Self {
65 Self::default()
66 }
67
68 pub fn gas(&mut self) -> Argument {
71 if let Some((index, arg)) = self.inputs.get(&InputArgKind::Gas) {
72 assert!(matches!(arg, InputArg::Gas));
73 Argument::new(*index)
74 } else {
75 let id = self.arguments.len();
76 self.arguments.insert(id, ResolvedArgument::Unresolved);
77 self.inputs.insert(InputArgKind::Gas, (id, InputArg::Gas));
78 Argument::new(id)
79 }
80 }
81
82 pub fn pure_bytes(&mut self, bytes: Vec<u8>) -> Argument {
83 match self.inputs.entry(InputArgKind::PureInput(bytes.clone())) {
84 std::collections::hash_map::Entry::Occupied(o) => {
85 assert!(matches!(o.get().1, InputArg::Pure(_)));
86 Argument::new(o.get().0)
87 }
88 std::collections::hash_map::Entry::Vacant(v) => {
89 let id = self.arguments.len();
90 self.arguments.insert(id, ResolvedArgument::Unresolved);
91 v.insert((id, InputArg::Pure(bytes)));
92 Argument::new(id)
93 }
94 }
95 }
96
97 pub fn pure<T: serde::Serialize>(&mut self, value: &T) -> Argument {
98 let bytes = bcs::to_bytes(value).expect("bcs serialization failed");
99 self.pure_bytes(bytes)
100 }
101
102 pub fn pure_bytes_unique(&mut self, bytes: Vec<u8>) -> Argument {
103 let id = self.arguments.len();
104 self.arguments.insert(id, ResolvedArgument::Unresolved);
105 self.inputs.insert(
106 InputArgKind::UniquePureInput(id),
107 (id, InputArg::Pure(bytes)),
108 );
109 Argument::new(id)
110 }
111
112 pub fn pure_unique<T: serde::Serialize>(&mut self, value: &T) -> Argument {
113 let bytes = bcs::to_bytes(value).expect("bcs serialization failed");
114 self.pure_bytes_unique(bytes)
115 }
116
117 pub fn object(&mut self, object: ObjectInput) -> Argument {
118 match self
119 .inputs
120 .entry(InputArgKind::ObjectInput(object.object_id))
121 {
122 std::collections::hash_map::Entry::Occupied(mut o) => {
123 let id = o.get().0;
124 let InputArg::Object(object2) = &mut o.get_mut().1 else {
125 panic!("BUG: invariant violation");
126 };
127
128 assert_eq!(
129 object.object_id, object2.object_id,
130 "BUG: invariant violation"
131 );
132
133 match (object.mutable, object2.mutable) {
134 (Some(_), None) => object2.mutable = object.mutable,
135 (Some(true), Some(false)) => object2.mutable = Some(true),
136 _ => {}
137 }
138
139 if let (Some(kind), None) = (object.kind, object2.kind) {
140 object2.kind = Some(kind);
141 }
142
143 if let (Some(version), None) = (object.version, object2.version) {
144 object2.version = Some(version);
145 }
146
147 if let (Some(digest), None) = (object.digest, object2.digest) {
148 object2.digest = Some(digest);
149 }
150
151 Argument::new(id)
152 }
153 std::collections::hash_map::Entry::Vacant(v) => {
154 let id = self.arguments.len();
155 self.arguments.insert(id, ResolvedArgument::Unresolved);
156 v.insert((id, InputArg::Object(object)));
157 Argument::new(id)
158 }
159 }
160 }
161
162 pub fn add_gas_objects<O, I>(&mut self, gas: I)
166 where
167 O: Into<ObjectInput>,
168 I: IntoIterator<Item = O>,
169 {
170 self.gas.extend(gas.into_iter().map(|x| x.into()));
171 }
172
173 pub fn set_gas_budget(&mut self, budget: u64) {
175 self.gas_budget = Some(budget);
176 }
177
178 pub fn set_gas_price(&mut self, price: u64) {
180 self.gas_price = Some(price);
181 }
182
183 pub fn set_sender(&mut self, sender: Address) {
185 self.sender = Some(sender);
186 }
187
188 pub fn set_sponsor(&mut self, sponsor: Address) {
190 self.sponsor = Some(sponsor);
191 }
192
193 pub fn set_expiration(&mut self, expiration: TransactionExpiration) {
195 self.expiration = Some(expiration);
196 }
197
198 fn command(&mut self, command: Command) -> Argument {
201 let id = self.arguments.len();
202 self.arguments.insert(id, ResolvedArgument::Unresolved);
203 self.commands.insert(id, command);
204 Argument::new(id)
205 }
206
207 pub fn move_call(&mut self, function: Function, arguments: Vec<Argument>) -> Argument {
216 let cmd = CommandKind::MoveCall(MoveCall {
217 package: function.package,
218 module: function.module,
219 function: function.function,
220 type_arguments: function.type_args,
221 arguments,
222 });
223 self.command(cmd.into())
224 }
225
226 pub fn transfer_objects(&mut self, objects: Vec<Argument>, address: Argument) {
228 let cmd = CommandKind::TransferObjects(TransferObjects { objects, address });
229 self.command(cmd.into());
230 }
231
232 pub fn split_coins(&mut self, coin: Argument, amounts: Vec<Argument>) -> Vec<Argument> {
236 let amounts_len = amounts.len();
237 let cmd = CommandKind::SplitCoins(SplitCoins { coin, amounts });
238 self.command(cmd.into()).to_nested(amounts_len)
239 }
240
241 pub fn merge_coins(&mut self, coin: Argument, coins_to_merge: Vec<Argument>) {
243 let cmd = CommandKind::MergeCoins(MergeCoins {
244 coin,
245 coins_to_merge,
246 });
247 self.command(cmd.into());
248 }
249
250 pub fn make_move_vec(&mut self, type_: Option<TypeTag>, elements: Vec<Argument>) -> Argument {
254 let cmd = CommandKind::MakeMoveVector(MakeMoveVector { type_, elements });
255 self.command(cmd.into())
256 }
257
258 pub fn publish(&mut self, modules: Vec<Vec<u8>>, dependencies: Vec<Address>) -> Argument {
270 let cmd = CommandKind::Publish(Publish {
271 modules,
272 dependencies,
273 });
274 self.command(cmd.into())
275 }
276
277 pub fn upgrade(
288 &mut self,
289 modules: Vec<Vec<u8>>,
290 dependencies: Vec<Address>,
291 package: Address,
292 ticket: Argument,
293 ) -> Argument {
294 let cmd = CommandKind::Upgrade(Upgrade {
295 modules,
296 dependencies,
297 package,
298 ticket,
299 });
300 self.command(cmd.into())
301 }
302
303 #[cfg(feature = "intents")]
308 #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
309 #[allow(private_bounds)]
310 pub fn intent<I: crate::intent::Intent>(&mut self, intent: I) -> Argument {
311 intent.register(self)
312 }
313
314 pub fn try_build(mut self) -> Result<Transaction, Error> {
319 let Some(sender) = self.sender else {
320 return Err(Error::MissingSender);
321 };
322 if self.gas.is_empty() {
323 return Err(Error::MissingGasObjects);
324 }
325 let Some(budget) = self.gas_budget else {
326 return Err(Error::MissingGasBudget);
327 };
328 let Some(price) = self.gas_price else {
329 return Err(Error::MissingGasPrice);
330 };
331
332 let gas_payment = sui_sdk_types::GasPayment {
334 objects: self
335 .gas
336 .iter()
337 .map(ObjectInput::try_into_object_reference)
338 .collect::<Result<Vec<_>, _>>()?,
339 owner: self.sponsor.unwrap_or(sender),
340 price,
341 budget,
342 };
343
344 if !self.intents.is_empty() {
346 return Err(Error::Input("unable to resolve intents offline".to_owned()));
347 }
348
349 let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
354 unresolved_inputs.sort_by_key(|(id, _input)| *id);
355
356 let mut resolved_inputs = Vec::new();
357 for (id, input) in unresolved_inputs {
358 let arg = match input {
359 InputArg::Gas => sui_sdk_types::Argument::Gas,
360 InputArg::Pure(value) => {
361 resolved_inputs.push(sui_sdk_types::Input::Pure(value));
362 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
363 }
364 InputArg::Object(object_input) => {
365 resolved_inputs.push(object_input.try_into_input()?);
366 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
367 }
368 };
369
370 *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
371 }
372
373 let mut resolved_commands = Vec::new();
378
379 for (id, command) in self.commands {
380 resolved_commands.push(
381 command
382 .try_resolve(&self.arguments)
383 .map_err(|e| e.unwrap_err())?,
384 );
385 let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
386
387 *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
388 }
389
390 Ok(Transaction {
391 kind: sui_sdk_types::TransactionKind::ProgrammableTransaction(
392 sui_sdk_types::ProgrammableTransaction {
393 inputs: resolved_inputs,
394 commands: resolved_commands,
395 },
396 ),
397 sender,
398 gas_payment,
399 expiration: self.expiration.unwrap_or(TransactionExpiration::None),
400 })
401 }
402
403 #[cfg(feature = "intents")]
404 #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
405 pub async fn build(mut self, client: &mut sui_rpc::Client) -> Result<Transaction, Error> {
406 use sui_rpc::field::FieldMask;
407 use sui_rpc::field::FieldMaskUtil;
408 use sui_rpc::proto::sui::rpc::v2::Input;
409 use sui_rpc::proto::sui::rpc::v2::SimulateTransactionRequest;
410 use sui_rpc::proto::sui::rpc::v2::SimulateTransactionResponse;
411 use sui_rpc::proto::sui::rpc::v2::input::InputKind;
412
413 let Some(sender) = self.sender else {
414 return Err(Error::MissingSender);
415 };
416
417 let mut request = SimulateTransactionRequest::default()
418 .with_read_mask(FieldMask::from_paths([
419 SimulateTransactionResponse::path_builder()
420 .transaction()
421 .transaction()
422 .finish(),
423 SimulateTransactionResponse::path_builder()
424 .transaction()
425 .effects()
426 .finish(),
427 ]))
428 .with_do_gas_selection(true);
429 request.transaction_mut().set_sender(sender);
430
431 let resolvers = std::mem::take(&mut self.resolvers);
439 for resolver in resolvers.values() {
440 resolver
441 .resolve(&mut self, client)
442 .await
443 .map_err(|e| Error::Input(e.to_string()))?;
444 }
445 if !self.intents.is_empty() {
447 return Err(Error::Input("unable to resolve all intents".to_owned()));
448 }
449
450 let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
455 unresolved_inputs.sort_by_key(|(id, _input)| *id);
456
457 let mut resolved_inputs = Vec::new();
458 for (id, input) in unresolved_inputs {
459 let arg = match input {
460 InputArg::Gas => sui_sdk_types::Argument::Gas,
461 InputArg::Pure(value) => {
462 resolved_inputs
463 .push(Input::default().with_kind(InputKind::Pure).with_pure(value));
464 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
465 }
466 InputArg::Object(object_input) => {
467 resolved_inputs.push(object_input.to_input_proto());
468 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
469 }
470 };
471
472 *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
473 }
474
475 let mut resolved_commands = Vec::new();
480
481 let mut stack = Vec::new();
482 let mut to_resolve = self.commands.pop_first();
483 while let Some((id, command)) = to_resolve.take() {
484 let resolved = match command.try_resolve(&self.arguments) {
485 Ok(resolved) => resolved,
486 Err(Ok(next)) => {
487 stack.push((id, command));
489 to_resolve = Some(
491 self.commands
492 .remove_entry(&next)
493 .expect("command must be there if it wasn't resolved yet"),
494 );
495 continue;
496 }
497 Err(Err(e)) => return Err(e),
498 };
499
500 resolved_commands.push(resolved);
501 let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
502 *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
503
504 if let Some(from_stack) = stack.pop() {
507 to_resolve = Some(from_stack);
508 } else {
509 to_resolve = self.commands.pop_first();
510 }
511 }
512
513 let t = request.transaction_mut();
514 t.kind_mut()
515 .programmable_transaction_mut()
516 .set_inputs(resolved_inputs);
517 t.kind_mut()
518 .programmable_transaction_mut()
519 .set_commands(resolved_commands.into_iter().map(Into::into).collect());
520
521 {
523 let payment = request.transaction_mut().gas_payment_mut();
524 payment.set_owner(self.sponsor.unwrap_or(sender));
525
526 if let Some(budget) = self.gas_budget {
527 payment.set_budget(budget);
528 }
529 if let Some(price) = self.gas_price {
530 payment.set_price(price);
531 };
532 payment.set_objects(
533 self.gas
534 .iter()
535 .map(ObjectInput::try_into_object_reference_proto)
536 .collect::<Result<_, _>>()?,
537 );
538 }
539
540 let response = client
541 .execution_client()
542 .simulate_transaction(request)
543 .await
544 .map_err(|e| Error::Input(format!("error simulating transaction: {e}")))?;
545
546 if !response
547 .get_ref()
548 .transaction()
549 .effects()
550 .status()
551 .success()
552 {
553 return Err(Error::Input(format!(
554 "txn failed to execute: {}",
555 response
556 .get_ref()
557 .transaction()
558 .effects()
559 .status()
560 .error()
561 .description()
562 )));
563 }
564
565 response
566 .get_ref()
567 .transaction()
568 .transaction()
569 .bcs()
570 .deserialize()
571 .map_err(|e| Error::Input(e.to_string()))
572 }
573
574 #[cfg(feature = "intents")]
575 pub(crate) fn register_resolver<R: crate::intent::IntentResolver>(&mut self, resolver: R) {
576 self.resolvers
577 .insert(resolver.type_id(), Box::new(resolver));
578 }
579
580 #[cfg(feature = "intents")]
581 pub(crate) fn unresolved<T: std::any::Any + Send + Sync>(&mut self, unresolved: T) -> Argument {
582 let id = self.arguments.len();
583 self.arguments.insert(id, ResolvedArgument::Unresolved);
584 self.intents.insert(id, Box::new(unresolved));
585 Argument::new(id)
586 }
587
588 #[cfg(feature = "intents")]
589 pub(crate) fn sender(&self) -> Option<Address> {
590 self.sender
591 }
592}
593
594#[derive(Clone, Copy, Debug)]
595pub struct Argument {
596 id: usize,
597 sub_index: Option<usize>,
598}
599
600impl Argument {
601 pub(crate) fn new(id: usize) -> Self {
602 Self {
603 id,
604 sub_index: None,
605 }
606 }
607
608 pub fn to_nested(self, count: usize) -> Vec<Self> {
609 (0..count)
610 .map(|sub_index| Argument {
611 sub_index: Some(sub_index),
612 ..self
613 })
614 .collect()
615 }
616
617 fn try_resolve(
618 self,
619 resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
620 ) -> Result<sui_sdk_types::Argument, Result<usize, Error>> {
621 let mut sub_index = self.sub_index;
622 let arg = {
623 let mut visited = BTreeSet::new();
624 let mut next_id = self.id;
625
626 loop {
627 if visited.contains(&next_id) {
628 panic!("BUG: cyclic dependency");
629 }
630 visited.insert(next_id);
631
632 match resolved_arguments.get(&next_id).unwrap() {
633 ResolvedArgument::Unresolved => return Err(Ok(next_id)),
634 ResolvedArgument::ReplaceWith(argument) => {
635 next_id = argument.id;
636 sub_index = argument.sub_index;
637 }
638 ResolvedArgument::Resolved(argument) => break argument,
639 }
640 }
641 };
642
643 if let Some(sub_index) = sub_index {
644 if let Some(arg) = arg.nested(sub_index as u16) {
645 return Ok(arg);
646 } else {
647 return Err(Err(Error::Input(
648 "unable to create nested argument".to_owned(),
649 )));
650 }
651 }
652
653 Ok(*arg)
654 }
655
656 fn try_resolve_many(
657 arguments: &[Self],
658 resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
659 ) -> Result<Vec<sui_sdk_types::Argument>, Result<usize, Error>> {
660 arguments
661 .iter()
662 .map(|a| a.try_resolve(resolved_arguments))
663 .collect::<Result<_, _>>()
664 }
665}
666
667pub(crate) struct Command {
668 kind: CommandKind,
669 pub(crate) dependencies: Vec<Argument>,
672}
673
674impl From<CommandKind> for Command {
675 fn from(value: CommandKind) -> Self {
676 Self {
677 kind: value,
678 dependencies: Vec::new(),
679 }
680 }
681}
682
683pub(crate) enum CommandKind {
684 MoveCall(MoveCall),
686
687 TransferObjects(TransferObjects),
692
693 SplitCoins(SplitCoins),
696
697 MergeCoins(MergeCoins),
700
701 Publish(Publish),
704
705 MakeMoveVector(MakeMoveVector),
709
710 Upgrade(Upgrade),
718}
719
720impl Command {
721 fn try_resolve(
722 &self,
723 resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
724 ) -> Result<sui_sdk_types::Command, Result<usize, Error>> {
725 use sui_sdk_types::Command as C;
726
727 Argument::try_resolve_many(&self.dependencies, resolved_arguments)?;
729
730 let cmd = match &self.kind {
731 CommandKind::MoveCall(MoveCall {
732 package,
733 module,
734 function,
735 type_arguments,
736 arguments,
737 }) => C::MoveCall(sui_sdk_types::MoveCall {
738 package: *package,
739 module: module.to_owned(),
740 function: function.to_owned(),
741 type_arguments: type_arguments.to_owned(),
742 arguments: Argument::try_resolve_many(arguments, resolved_arguments)?,
743 }),
744
745 CommandKind::TransferObjects(TransferObjects { objects, address }) => {
746 C::TransferObjects(sui_sdk_types::TransferObjects {
747 objects: Argument::try_resolve_many(objects, resolved_arguments)?,
748 address: address.try_resolve(resolved_arguments)?,
749 })
750 }
751
752 CommandKind::SplitCoins(SplitCoins { coin, amounts }) => {
753 C::SplitCoins(sui_sdk_types::SplitCoins {
754 coin: coin.try_resolve(resolved_arguments)?,
755 amounts: Argument::try_resolve_many(amounts, resolved_arguments)?,
756 })
757 }
758
759 CommandKind::MergeCoins(MergeCoins {
760 coin,
761 coins_to_merge,
762 }) => C::MergeCoins(sui_sdk_types::MergeCoins {
763 coin: coin.try_resolve(resolved_arguments)?,
764 coins_to_merge: Argument::try_resolve_many(coins_to_merge, resolved_arguments)?,
765 }),
766
767 CommandKind::Publish(Publish {
768 modules,
769 dependencies,
770 }) => C::Publish(sui_sdk_types::Publish {
771 modules: modules.to_owned(),
772 dependencies: dependencies.to_owned(),
773 }),
774
775 CommandKind::MakeMoveVector(MakeMoveVector { type_, elements }) => {
776 C::MakeMoveVector(sui_sdk_types::MakeMoveVector {
777 type_: type_.to_owned(),
778 elements: Argument::try_resolve_many(elements, resolved_arguments)?,
779 })
780 }
781
782 CommandKind::Upgrade(Upgrade {
783 modules,
784 dependencies,
785 package,
786 ticket,
787 }) => C::Upgrade(sui_sdk_types::Upgrade {
788 modules: modules.to_owned(),
789 dependencies: dependencies.to_owned(),
790 package: *package,
791 ticket: ticket.try_resolve(resolved_arguments)?,
792 }),
793 };
794 Ok(cmd)
795 }
796}
797
798pub(crate) struct TransferObjects {
799 pub objects: Vec<Argument>,
801
802 pub address: Argument,
804}
805
806pub(crate) struct SplitCoins {
807 pub coin: Argument,
809
810 pub amounts: Vec<Argument>,
812}
813
814pub(crate) struct MergeCoins {
815 pub coin: Argument,
817
818 pub coins_to_merge: Vec<Argument>,
822}
823
824pub(crate) struct Publish {
825 pub modules: Vec<Vec<u8>>,
827
828 pub dependencies: Vec<Address>,
830}
831
832pub(crate) struct MakeMoveVector {
833 pub type_: Option<TypeTag>,
838
839 pub elements: Vec<Argument>,
841}
842
843pub(crate) struct Upgrade {
844 pub modules: Vec<Vec<u8>>,
846
847 pub dependencies: Vec<Address>,
849
850 pub package: Address,
852
853 pub ticket: Argument,
855}
856
857pub(crate) struct MoveCall {
858 pub package: Address,
860
861 pub module: Identifier,
863
864 pub function: Identifier,
866
867 pub type_arguments: Vec<TypeTag>,
869
870 pub arguments: Vec<Argument>,
872 }
874
875pub struct ObjectInput {
876 object_id: Address,
877 kind: Option<ObjectKind>,
878 version: Option<u64>,
879 digest: Option<Digest>,
880 mutable: Option<bool>,
881}
882
883#[derive(Clone, Copy)]
884enum ObjectKind {
885 Shared,
886 Receiving,
887 ImmutableOrOwned,
888}
889
890impl ObjectInput {
891 pub fn new(object_id: Address) -> Self {
892 Self {
893 kind: None,
894 object_id,
895 version: None,
896 digest: None,
897 mutable: None,
898 }
899 }
900
901 pub fn owned(object_id: Address, version: u64, digest: Digest) -> Self {
903 Self {
904 kind: Some(ObjectKind::ImmutableOrOwned),
905 object_id,
906 version: Some(version),
907 digest: Some(digest),
908 mutable: None,
909 }
910 }
911
912 pub fn immutable(object_id: Address, version: u64, digest: Digest) -> Self {
914 Self {
915 kind: Some(ObjectKind::ImmutableOrOwned),
916 object_id,
917 version: Some(version),
918 digest: Some(digest),
919 mutable: None,
920 }
921 }
922
923 pub fn receiving(object_id: Address, version: u64, digest: Digest) -> Self {
925 Self {
926 kind: Some(ObjectKind::Receiving),
927 object_id,
928 version: Some(version),
929 digest: Some(digest),
930 mutable: None,
931 }
932 }
933
934 pub fn shared(object_id: Address, version: u64, mutable: bool) -> Self {
938 Self {
939 kind: Some(ObjectKind::Shared),
940 object_id,
941 version: Some(version),
942 mutable: Some(mutable),
943 digest: None,
944 }
945 }
946
947 pub fn as_immutable(self) -> Self {
949 Self {
950 kind: Some(ObjectKind::ImmutableOrOwned),
951 ..self
952 }
953 }
954
955 pub fn as_owned(self) -> Self {
957 Self {
958 kind: Some(ObjectKind::ImmutableOrOwned),
959 ..self
960 }
961 }
962
963 pub fn as_receiving(self) -> Self {
965 Self {
966 kind: Some(ObjectKind::Receiving),
967 ..self
968 }
969 }
970
971 pub fn as_shared(self) -> Self {
973 Self {
974 kind: Some(ObjectKind::Shared),
975 ..self
976 }
977 }
978
979 pub fn with_version(self, version: u64) -> Self {
981 Self {
982 version: Some(version),
983 ..self
984 }
985 }
986
987 pub fn with_digest(self, digest: Digest) -> Self {
989 Self {
990 digest: Some(digest),
991 ..self
992 }
993 }
994
995 pub fn with_mutable(self, mutable: bool) -> Self {
996 Self {
997 mutable: Some(mutable),
998 ..self
999 }
1000 }
1001}
1002
1003impl From<&sui_sdk_types::Object> for ObjectInput {
1004 fn from(object: &sui_sdk_types::Object) -> Self {
1005 let input = Self::new(object.object_id())
1006 .with_version(object.version())
1007 .with_digest(object.digest());
1008
1009 match object.owner() {
1010 sui_sdk_types::Owner::Address(_) => input.as_owned(),
1011 sui_sdk_types::Owner::Object(_) => input,
1012 sui_sdk_types::Owner::Shared(version) => input.with_version(*version).as_shared(),
1013 sui_sdk_types::Owner::Immutable => input.as_immutable(),
1014 sui_sdk_types::Owner::ConsensusAddress { start_version, .. } => {
1015 input.with_version(*start_version).as_shared()
1016 }
1017 _ => input,
1018 }
1019 }
1020}
1021
1022impl ObjectInput {
1032 fn try_into_object_reference(&self) -> Result<sui_sdk_types::ObjectReference, Error> {
1033 if matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None)
1034 && let Some(version) = self.version
1035 && let Some(digest) = self.digest
1036 {
1037 Ok(sui_sdk_types::ObjectReference::new(
1038 self.object_id,
1039 version,
1040 digest,
1041 ))
1042 } else {
1043 Err(Error::WrongGasObject)
1044 }
1045 }
1046
1047 fn try_into_input(&self) -> Result<sui_sdk_types::Input, Error> {
1048 let input = match self {
1049 Self {
1051 object_id,
1052 kind: Some(ObjectKind::ImmutableOrOwned),
1053 version: Some(version),
1054 digest: Some(digest),
1055 ..
1056 }
1057 | Self {
1058 object_id,
1059 kind: None,
1060 version: Some(version),
1061 digest: Some(digest),
1062 mutable: None,
1063 } => sui_sdk_types::Input::ImmutableOrOwned(sui_sdk_types::ObjectReference::new(
1064 *object_id, *version, *digest,
1065 )),
1066
1067 Self {
1069 object_id,
1070 kind: Some(ObjectKind::Receiving),
1071 version: Some(version),
1072 digest: Some(digest),
1073 ..
1074 } => sui_sdk_types::Input::Receiving(sui_sdk_types::ObjectReference::new(
1075 *object_id, *version, *digest,
1076 )),
1077
1078 Self {
1080 object_id,
1081 kind: Some(ObjectKind::Shared),
1082 version: Some(version),
1083 mutable: Some(mutable),
1084 ..
1085 }
1086 | Self {
1087 object_id,
1088 kind: None,
1089 version: Some(version),
1090 digest: None,
1091 mutable: Some(mutable),
1092 } => sui_sdk_types::Input::Shared(sui_sdk_types::SharedInput::new(
1093 *object_id, *version, *mutable,
1094 )),
1095
1096 _ => {
1097 return Err(Error::Input(format!(
1098 "Input object {} is incomplete",
1099 self.object_id
1100 )));
1101 }
1102 };
1103 Ok(input)
1104 }
1105
1106 #[cfg(feature = "intents")]
1107 fn to_input_proto(&self) -> sui_rpc::proto::sui::rpc::v2::Input {
1108 use sui_rpc::proto::sui::rpc::v2::input::InputKind;
1109
1110 let mut input =
1111 sui_rpc::proto::sui::rpc::v2::Input::default().with_object_id(self.object_id);
1112 match self.kind {
1113 Some(ObjectKind::Shared) => input.set_kind(InputKind::Shared),
1114 Some(ObjectKind::Receiving) => input.set_kind(InputKind::Receiving),
1115 Some(ObjectKind::ImmutableOrOwned) => input.set_kind(InputKind::ImmutableOrOwned),
1116 None => {}
1117 }
1118
1119 if let Some(version) = self.version {
1120 input.set_version(version);
1121 }
1122
1123 if let Some(digest) = self.digest {
1124 input.set_digest(digest);
1125 }
1126
1127 if let Some(mutable) = self.mutable {
1128 input.set_mutable(mutable);
1129 }
1130
1131 input
1132 }
1133
1134 #[cfg(feature = "intents")]
1135 fn try_into_object_reference_proto(
1136 &self,
1137 ) -> Result<sui_rpc::proto::sui::rpc::v2::ObjectReference, Error> {
1138 if !matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None) {
1139 return Err(Error::WrongGasObject);
1140 }
1141
1142 let mut input =
1143 sui_rpc::proto::sui::rpc::v2::ObjectReference::default().with_object_id(self.object_id);
1144 if let Some(version) = self.version {
1145 input.set_version(version);
1146 }
1147 if let Some(digest) = self.digest {
1148 input.set_digest(digest);
1149 }
1150 Ok(input)
1151 }
1152
1153 #[cfg(feature = "intents")]
1154 pub(crate) fn try_from_object_proto(
1155 object: &sui_rpc::proto::sui::rpc::v2::Object,
1156 ) -> Result<Self, Error> {
1157 use sui_rpc::proto::sui::rpc::v2::owner::OwnerKind;
1158
1159 let input = Self::new(
1160 object
1161 .object_id()
1162 .parse()
1163 .map_err(|_e| Error::MissingObjectId)?,
1164 );
1165
1166 Ok(match object.owner().kind() {
1167 OwnerKind::Address | OwnerKind::Immutable => {
1168 input.as_owned().with_version(object.version()).with_digest(
1169 object
1170 .digest()
1171 .parse()
1172 .map_err(|_| Error::Input("can't parse digest".to_owned()))?,
1173 )
1174 }
1175 OwnerKind::Object => return Err(Error::Input("invalid object type".to_owned())),
1176 OwnerKind::Shared | OwnerKind::ConsensusAddress => input
1177 .as_shared()
1178 .with_version(object.owner().version())
1179 .with_mutable(true),
1180 OwnerKind::Unknown | _ => input,
1181 })
1182 }
1183}
1184
1185pub struct Function {
1187 package: Address,
1189 module: Identifier,
1191 function: Identifier,
1193 type_args: Vec<TypeTag>,
1195}
1196
1197impl Function {
1198 pub fn new(package: Address, module: Identifier, function: Identifier) -> Self {
1200 Self {
1201 package,
1202 module,
1203 function,
1204 type_args: Vec::new(),
1205 }
1206 }
1207
1208 pub fn with_type_args(self, type_args: Vec<TypeTag>) -> Self {
1209 Self { type_args, ..self }
1210 }
1211}
1212
1213#[cfg(test)]
1214mod tests {
1215 use super::*;
1216
1217 #[test]
1218 fn simple_try_build() {
1219 let mut tx = TransactionBuilder::new();
1220 let _coin = tx.object(ObjectInput::owned(
1221 Address::from_static(
1222 "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1223 ),
1224 2,
1225 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1226 ));
1227 let _gas = tx.gas();
1228
1229 let _recipient = tx.pure(&Address::from_static("0xabc"));
1230
1231 assert!(tx.try_build().is_err());
1232
1233 let mut tx = TransactionBuilder::new();
1234 let coin = tx.object(ObjectInput::owned(
1235 Address::from_static(
1236 "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1237 ),
1238 2,
1239 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1240 ));
1241 let gas = tx.gas();
1242
1243 let recipient = tx.pure(&Address::from_static("0xabc"));
1244 tx.transfer_objects(vec![coin, gas], recipient);
1245 tx.set_gas_budget(500000000);
1246 tx.set_gas_price(1000);
1247 tx.add_gas_objects([ObjectInput::owned(
1248 Address::from_static(
1249 "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1250 ),
1251 2,
1252 Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1253 )]);
1254 tx.set_sender(Address::from_static(
1255 "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1256 ));
1257
1258 assert!(tx.try_build().is_ok());
1259 }
1260
1261 #[test]
1262 fn test_split_transfer() {
1263 let mut tx = TransactionBuilder::new();
1264
1265 let amount = tx.pure(&1_000_000_000u64);
1267 let gas = tx.gas();
1268 let result = tx.split_coins(gas, vec![amount; 5]);
1269 let recipient = tx.pure(&Address::from_static("0xabc"));
1270 tx.transfer_objects(result, recipient);
1271
1272 tx.set_gas_budget(500000000);
1273 tx.set_gas_price(1000);
1274 tx.add_gas_objects([ObjectInput::owned(
1275 Address::from_static(
1276 "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1277 ),
1278 2,
1279 Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1280 )]);
1281 tx.set_sender(Address::from_static(
1282 "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1283 ));
1284
1285 assert!(tx.try_build().is_ok());
1286 }
1287
1288 #[test]
1289 fn test_deterministic_building() {
1290 let build_tx = || {
1291 let mut tx = TransactionBuilder::new();
1292 let coin = tx.object(ObjectInput::owned(
1293 Address::from_static(
1294 "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1295 ),
1296 2,
1297 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1298 ));
1299 let _ = tx.object(ObjectInput::owned(
1300 Address::from_static("0x12345"),
1301 2,
1302 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1303 ));
1304 let _ = tx.object(ObjectInput::owned(
1305 Address::from_static("0x12345"),
1306 2,
1307 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1308 ));
1309 let gas = tx.gas();
1310 let _ = tx.pure(&Address::from_static("0xabc"));
1311 let _ = tx.pure(&Address::from_static("0xabc"));
1312 let _ = tx.pure(&Address::from_static("0xabc"));
1313 let _ = tx.pure(&Address::from_static("0xdef"));
1314 let _ = tx.pure(&1u64);
1315 let _ = tx.pure(&1u64);
1316 let _ = tx.pure(&1u64);
1317 let _ = tx.pure(&Some(2u8));
1318 let _ = tx.pure_unique(&Address::from_static("0xabc"));
1319 let _ = tx.pure_unique(&Address::from_static("0xabc"));
1320 let _ = tx.pure_unique(&1u64);
1321
1322 let recipient = tx.pure(&Address::from_static("0x123"));
1323 tx.transfer_objects(vec![coin, gas], recipient);
1324 tx.set_gas_budget(500000000);
1325 tx.set_gas_price(1000);
1326 tx.add_gas_objects([ObjectInput::owned(
1327 Address::from_static(
1328 "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1329 ),
1330 2,
1331 Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1332 )]);
1333 tx.set_sender(Address::from_static(
1334 "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1335 ));
1336
1337 tx.try_build().unwrap()
1338 };
1339
1340 let digest = build_tx().digest();
1341
1342 assert!(
1343 (0..100)
1344 .map(|_| build_tx())
1345 .map(|tx| tx.digest())
1346 .all(|d| d == digest)
1347 )
1348 }
1349}