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