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)]
47pub struct TransactionBuilder {
48 pub(crate) gas: Vec<ObjectInput>,
52 gas_budget: Option<u64>,
54 gas_price: Option<u64>,
56 sender: Option<Address>,
58 sponsor: Option<Address>,
60 expiration: Option<TransactionExpiration>,
62
63 #[cfg(feature = "intents")]
65 pub(crate) resolvers: BTreeMap<std::any::TypeId, Box<dyn crate::intent::IntentResolver>>,
66
67 pub(crate) arguments: BTreeMap<usize, ResolvedArgument>,
68 inputs: HashMap<InputArgKind, (usize, InputArg)>,
69 pub(crate) commands: BTreeMap<usize, Command>,
70 pub(crate) intents: BTreeMap<usize, Box<dyn std::any::Any + Send + Sync>>,
71}
72
73#[derive(Clone, Copy, Debug)]
74pub(crate) enum ResolvedArgument {
75 Unresolved,
76 #[allow(unused)]
77 ReplaceWith(Argument),
78 Resolved(sui_sdk_types::Argument),
79}
80
81#[derive(Debug, PartialEq, Eq, Hash)]
82pub(crate) enum InputArgKind {
83 Gas,
84 ObjectInput(Address),
85 PureInput(Vec<u8>),
86 UniquePureInput(usize),
87 FundsWithdrawal(usize),
89}
90
91pub(crate) enum InputArg {
92 Gas,
93 Pure(Vec<u8>),
94 Object(ObjectInput),
95 FundsWithdrawal(sui_sdk_types::FundsWithdrawal),
96}
97
98impl TransactionBuilder {
99 pub fn new() -> Self {
107 Self::default()
108 }
109
110 pub fn gas(&mut self) -> Argument {
126 if let Some((index, arg)) = self.inputs.get(&InputArgKind::Gas) {
127 assert!(matches!(arg, InputArg::Gas));
128 Argument::new(*index)
129 } else {
130 let id = self.arguments.len();
131 self.arguments.insert(id, ResolvedArgument::Unresolved);
132 self.inputs.insert(InputArgKind::Gas, (id, InputArg::Gas));
133 Argument::new(id)
134 }
135 }
136
137 pub fn pure_bytes(&mut self, bytes: Vec<u8>) -> Argument {
152 match self.inputs.entry(InputArgKind::PureInput(bytes.clone())) {
153 std::collections::hash_map::Entry::Occupied(o) => {
154 assert!(matches!(o.get().1, InputArg::Pure(_)));
155 Argument::new(o.get().0)
156 }
157 std::collections::hash_map::Entry::Vacant(v) => {
158 let id = self.arguments.len();
159 self.arguments.insert(id, ResolvedArgument::Unresolved);
160 v.insert((id, InputArg::Pure(bytes)));
161 Argument::new(id)
162 }
163 }
164 }
165
166 pub fn pure<T: serde::Serialize>(&mut self, value: &T) -> Argument {
181 let bytes = bcs::to_bytes(value).expect("bcs serialization failed");
182 self.pure_bytes(bytes)
183 }
184
185 pub fn pure_bytes_unique(&mut self, bytes: Vec<u8>) -> Argument {
199 let id = self.arguments.len();
200 self.arguments.insert(id, ResolvedArgument::Unresolved);
201 self.inputs.insert(
202 InputArgKind::UniquePureInput(id),
203 (id, InputArg::Pure(bytes)),
204 );
205 Argument::new(id)
206 }
207
208 pub fn pure_unique<T: serde::Serialize>(&mut self, value: &T) -> Argument {
221 let bytes = bcs::to_bytes(value).expect("bcs serialization failed");
222 self.pure_bytes_unique(bytes)
223 }
224
225 pub fn object(&mut self, object: ObjectInput) -> Argument {
241 match self
242 .inputs
243 .entry(InputArgKind::ObjectInput(object.object_id))
244 {
245 std::collections::hash_map::Entry::Occupied(mut o) => {
246 let id = o.get().0;
247 let InputArg::Object(object2) = &mut o.get_mut().1 else {
248 panic!("BUG: invariant violation");
249 };
250
251 assert_eq!(
252 object.object_id, object2.object_id,
253 "BUG: invariant violation"
254 );
255
256 match (object.mutable, object2.mutable) {
257 (Some(_), None) => object2.mutable = object.mutable,
258 (Some(true), Some(false)) => object2.mutable = Some(true),
259 _ => {}
260 }
261
262 if let (Some(kind), None) = (object.kind, object2.kind) {
263 object2.kind = Some(kind);
264 }
265
266 if let (Some(version), None) = (object.version, object2.version) {
267 object2.version = Some(version);
268 }
269
270 if let (Some(digest), None) = (object.digest, object2.digest) {
271 object2.digest = Some(digest);
272 }
273
274 Argument::new(id)
275 }
276 std::collections::hash_map::Entry::Vacant(v) => {
277 let id = self.arguments.len();
278 self.arguments.insert(id, ResolvedArgument::Unresolved);
279 v.insert((id, InputArg::Object(object)));
280 Argument::new(id)
281 }
282 }
283 }
284
285 pub fn funds_withdrawal(&mut self, coin_type: TypeTag, amount: u64) -> Argument {
293 let funds_withdrawal = sui_sdk_types::FundsWithdrawal::new(
294 amount,
295 coin_type,
296 sui_sdk_types::WithdrawFrom::Sender,
297 );
298
299 let id = self.arguments.len();
300 self.arguments.insert(id, ResolvedArgument::Unresolved);
301 self.inputs.insert(
302 InputArgKind::FundsWithdrawal(id),
303 (id, InputArg::FundsWithdrawal(funds_withdrawal)),
304 );
305 Argument::new(id)
306 }
307
308 pub fn funds_withdrawal_coin(&mut self, coin_type: TypeTag, amount: u64) -> Argument {
313 let withdrawal = self.funds_withdrawal(coin_type.clone(), amount);
314 self.move_call(
315 Function {
316 package: const { Address::from_static("0x2") },
317 module: const { Identifier::from_static("coin") },
318 function: const { Identifier::from_static("redeem_funds") },
319 type_args: vec![coin_type],
320 },
321 vec![withdrawal],
322 )
323 }
324
325 pub fn funds_withdrawal_balance(&mut self, coin_type: TypeTag, amount: u64) -> Argument {
330 let withdrawal = self.funds_withdrawal(coin_type.clone(), amount);
331 self.move_call(
332 Function {
333 package: const { Address::from_static("0x2") },
334 module: const { Identifier::from_static("balance") },
335 function: const { Identifier::from_static("redeem_funds") },
336 type_args: vec![coin_type],
337 },
338 vec![withdrawal],
339 )
340 }
341
342 pub fn add_gas_objects<O, I>(&mut self, gas: I)
356 where
357 O: Into<ObjectInput>,
358 I: IntoIterator<Item = O>,
359 {
360 self.gas.extend(gas.into_iter().map(|x| x.into()));
361 }
362
363 pub fn set_gas_budget(&mut self, budget: u64) {
372 self.gas_budget = Some(budget);
373 }
374
375 pub fn set_gas_price(&mut self, price: u64) {
384 self.gas_price = Some(price);
385 }
386
387 pub fn set_sender(&mut self, sender: Address) {
397 self.sender = Some(sender);
398 }
399
400 pub fn set_sponsor(&mut self, sponsor: Address) {
404 self.sponsor = Some(sponsor);
405 }
406
407 pub fn set_expiration(&mut self, expiration: TransactionExpiration) {
409 self.expiration = Some(expiration);
410 }
411
412 fn command(&mut self, command: Command) -> Argument {
415 let id = self.arguments.len();
416 self.arguments.insert(id, ResolvedArgument::Unresolved);
417 self.commands.insert(id, command);
418 Argument::new(id)
419 }
420
421 pub fn move_call(&mut self, function: Function, arguments: Vec<Argument>) -> Argument {
447 let cmd = CommandKind::MoveCall(MoveCall {
448 package: function.package,
449 module: function.module,
450 function: function.function,
451 type_arguments: function.type_args,
452 arguments,
453 });
454 self.command(cmd.into())
455 }
456
457 pub fn transfer_objects(&mut self, objects: Vec<Argument>, address: Argument) {
471 let cmd = CommandKind::TransferObjects(TransferObjects { objects, address });
472 self.command(cmd.into());
473 }
474
475 pub fn split_coins(&mut self, coin: Argument, amounts: Vec<Argument>) -> Vec<Argument> {
490 let amounts_len = amounts.len();
491 let cmd = CommandKind::SplitCoins(SplitCoins { coin, amounts });
492 self.command(cmd.into()).to_nested(amounts_len)
493 }
494
495 pub fn merge_coins(&mut self, coin: Argument, coins_to_merge: Vec<Argument>) {
513 let cmd = CommandKind::MergeCoins(MergeCoins {
514 coin,
515 coins_to_merge,
516 });
517 self.command(cmd.into());
518 }
519
520 pub fn make_move_vec(&mut self, type_: Option<TypeTag>, elements: Vec<Argument>) -> Argument {
534 let cmd = CommandKind::MakeMoveVector(MakeMoveVector { type_, elements });
535 self.command(cmd.into())
536 }
537
538 pub fn publish(&mut self, modules: Vec<Vec<u8>>, dependencies: Vec<Address>) -> Argument {
550 let cmd = CommandKind::Publish(Publish {
551 modules,
552 dependencies,
553 });
554 self.command(cmd.into())
555 }
556
557 pub fn upgrade(
568 &mut self,
569 modules: Vec<Vec<u8>>,
570 dependencies: Vec<Address>,
571 package: Address,
572 ticket: Argument,
573 ) -> Argument {
574 let cmd = CommandKind::Upgrade(Upgrade {
575 modules,
576 dependencies,
577 package,
578 ticket,
579 });
580 self.command(cmd.into())
581 }
582
583 #[cfg(feature = "intents")]
601 #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
602 #[allow(private_bounds)]
603 pub fn intent<I: crate::intent::Intent>(&mut self, intent: I) -> Argument {
604 intent.register(self)
605 }
606
607 pub fn try_build(mut self) -> Result<Transaction, Error> {
637 let Some(sender) = self.sender else {
638 return Err(Error::MissingSender);
639 };
640 if self.gas.is_empty() {
641 return Err(Error::MissingGasObjects);
642 }
643 let Some(budget) = self.gas_budget else {
644 return Err(Error::MissingGasBudget);
645 };
646 let Some(price) = self.gas_price else {
647 return Err(Error::MissingGasPrice);
648 };
649
650 let gas_payment = sui_sdk_types::GasPayment {
652 objects: self
653 .gas
654 .iter()
655 .map(ObjectInput::try_into_object_reference)
656 .collect::<Result<Vec<_>, _>>()?,
657 owner: self.sponsor.unwrap_or(sender),
658 price,
659 budget,
660 };
661
662 if !self.intents.is_empty() {
664 return Err(Error::Input("unable to resolve intents offline".to_owned()));
665 }
666
667 let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
672 unresolved_inputs.sort_by_key(|(id, _input)| *id);
673
674 let mut resolved_inputs = Vec::new();
675 for (id, input) in unresolved_inputs {
676 let arg = match input {
677 InputArg::Gas => sui_sdk_types::Argument::Gas,
678 InputArg::Pure(value) => {
679 resolved_inputs.push(sui_sdk_types::Input::Pure(value));
680 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
681 }
682 InputArg::Object(object_input) => {
683 resolved_inputs.push(object_input.try_into_input()?);
684 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
685 }
686 InputArg::FundsWithdrawal(funds_withdrawal) => {
687 resolved_inputs.push(sui_sdk_types::Input::FundsWithdrawal(funds_withdrawal));
688 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
689 }
690 };
691
692 *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
693 }
694
695 let mut resolved_commands = Vec::new();
700
701 for (id, command) in self.commands {
702 resolved_commands.push(
703 command
704 .try_resolve(&self.arguments)
705 .map_err(|e| e.unwrap_err())?,
706 );
707 let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
708
709 *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
710 }
711
712 Ok(Transaction {
713 kind: sui_sdk_types::TransactionKind::ProgrammableTransaction(
714 sui_sdk_types::ProgrammableTransaction {
715 inputs: resolved_inputs,
716 commands: resolved_commands,
717 },
718 ),
719 sender,
720 gas_payment,
721 expiration: self.expiration.unwrap_or(TransactionExpiration::None),
722 })
723 }
724
725 #[cfg(feature = "intents")]
737 #[cfg_attr(doc_cfg, doc(cfg(feature = "intents")))]
738 pub async fn build(mut self, client: &mut sui_rpc::Client) -> Result<Transaction, Error> {
739 use sui_rpc::field::FieldMask;
740 use sui_rpc::field::FieldMaskUtil;
741 use sui_rpc::proto::sui::rpc::v2::Input;
742 use sui_rpc::proto::sui::rpc::v2::SimulateTransactionRequest;
743 use sui_rpc::proto::sui::rpc::v2::SimulateTransactionResponse;
744 use sui_rpc::proto::sui::rpc::v2::input::InputKind;
745
746 let Some(sender) = self.sender else {
747 return Err(Error::MissingSender);
748 };
749
750 let mut request = SimulateTransactionRequest::default()
751 .with_read_mask(FieldMask::from_paths([
752 SimulateTransactionResponse::path_builder()
753 .transaction()
754 .transaction()
755 .finish(),
756 SimulateTransactionResponse::path_builder()
757 .transaction()
758 .effects()
759 .finish(),
760 ]))
761 .with_do_gas_selection(true);
762 request.transaction_mut().set_sender(sender);
763
764 let resolvers = std::mem::take(&mut self.resolvers);
772 for resolver in resolvers.values() {
773 resolver
774 .resolve(&mut self, client)
775 .await
776 .map_err(|e| Error::Input(e.to_string()))?;
777 }
778 if !self.intents.is_empty() {
780 return Err(Error::Input("unable to resolve all intents".to_owned()));
781 }
782
783 let mut unresolved_inputs = self.inputs.into_values().collect::<Vec<_>>();
788 unresolved_inputs.sort_by_key(|(id, _input)| *id);
789
790 let mut resolved_inputs = Vec::new();
791 for (id, input) in unresolved_inputs {
792 let arg = match input {
793 InputArg::Gas => sui_sdk_types::Argument::Gas,
794 InputArg::Pure(value) => {
795 resolved_inputs
796 .push(Input::default().with_kind(InputKind::Pure).with_pure(value));
797 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
798 }
799 InputArg::Object(object_input) => {
800 resolved_inputs.push(object_input.to_input_proto());
801 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
802 }
803 InputArg::FundsWithdrawal(funds_withdrawal) => {
804 resolved_inputs
805 .push(sui_sdk_types::Input::FundsWithdrawal(funds_withdrawal).into());
806 sui_sdk_types::Argument::Input(resolved_inputs.len() as u16 - 1)
807 }
808 };
809
810 *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
811 }
812
813 let mut resolved_commands = Vec::new();
818
819 let mut stack = Vec::new();
820 let mut to_resolve = self.commands.pop_first();
821 while let Some((id, command)) = to_resolve.take() {
822 let resolved = match command.try_resolve(&self.arguments) {
823 Ok(resolved) => resolved,
824 Err(Ok(next)) => {
825 stack.push((id, command));
827 to_resolve = Some(
829 self.commands
830 .remove_entry(&next)
831 .expect("command must be there if it wasn't resolved yet"),
832 );
833 continue;
834 }
835 Err(Err(e)) => return Err(e),
836 };
837
838 resolved_commands.push(resolved);
839 let arg = sui_sdk_types::Argument::Result(resolved_commands.len() as u16 - 1);
840 *self.arguments.get_mut(&id).unwrap() = ResolvedArgument::Resolved(arg);
841
842 if let Some(from_stack) = stack.pop() {
845 to_resolve = Some(from_stack);
846 } else {
847 to_resolve = self.commands.pop_first();
848 }
849 }
850
851 let t = request.transaction_mut();
852 t.kind_mut()
853 .programmable_transaction_mut()
854 .set_inputs(resolved_inputs);
855 t.kind_mut()
856 .programmable_transaction_mut()
857 .set_commands(resolved_commands.into_iter().map(Into::into).collect());
858
859 {
861 let payment = request.transaction_mut().gas_payment_mut();
862 payment.set_owner(self.sponsor.unwrap_or(sender));
863
864 if let Some(budget) = self.gas_budget {
865 payment.set_budget(budget);
866 }
867 if let Some(price) = self.gas_price {
868 payment.set_price(price);
869 };
870 payment.set_objects(
871 self.gas
872 .iter()
873 .map(ObjectInput::try_into_object_reference_proto)
874 .collect::<Result<_, _>>()?,
875 );
876 }
877
878 let response = client
879 .execution_client()
880 .simulate_transaction(request)
881 .await
882 .map_err(|e| Error::Input(format!("error simulating transaction: {e}")))?;
883
884 if !response
885 .get_ref()
886 .transaction()
887 .effects()
888 .status()
889 .success()
890 {
891 let error = response
892 .get_ref()
893 .transaction()
894 .effects()
895 .status()
896 .error()
897 .clone();
898 return Err(Error::SimulationFailure(Box::new(
899 crate::error::SimulationFailure::new(error),
900 )));
901 }
902
903 response
904 .get_ref()
905 .transaction()
906 .transaction()
907 .bcs()
908 .deserialize()
909 .map_err(|e| Error::Input(e.to_string()))
910 }
911
912 #[cfg(feature = "intents")]
913 pub(crate) fn register_resolver<R: crate::intent::IntentResolver>(&mut self, resolver: R) {
914 self.resolvers
915 .insert(resolver.type_id(), Box::new(resolver));
916 }
917
918 #[cfg(feature = "intents")]
919 pub(crate) fn unresolved<T: std::any::Any + Send + Sync>(&mut self, unresolved: T) -> Argument {
920 let id = self.arguments.len();
921 self.arguments.insert(id, ResolvedArgument::Unresolved);
922 self.intents.insert(id, Box::new(unresolved));
923 Argument::new(id)
924 }
925
926 #[cfg(feature = "intents")]
927 pub(crate) fn sender(&self) -> Option<Address> {
928 self.sender
929 }
930
931 #[cfg(feature = "intents")]
933 pub(crate) fn used_object_ids(&self) -> std::collections::HashSet<Address> {
934 let gas_ids = self.gas.iter().map(|o| o.object_id());
935 let input_ids = self.inputs.values().filter_map(|(_, input)| match input {
936 InputArg::Object(o) => Some(o.object_id()),
937 _ => None,
938 });
939 gas_ids.chain(input_ids).collect()
940 }
941}
942
943#[derive(Clone, Copy, Debug)]
952pub struct Argument {
953 id: usize,
954 sub_index: Option<usize>,
955}
956
957impl Argument {
958 pub(crate) fn new(id: usize) -> Self {
959 Self {
960 id,
961 sub_index: None,
962 }
963 }
964
965 pub fn to_nested(self, count: usize) -> Vec<Self> {
992 (0..count)
993 .map(|sub_index| Argument {
994 sub_index: Some(sub_index),
995 ..self
996 })
997 .collect()
998 }
999
1000 fn try_resolve(
1001 self,
1002 resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1003 ) -> Result<sui_sdk_types::Argument, Result<usize, Error>> {
1004 let mut sub_index = self.sub_index;
1005 let arg = {
1006 let mut visited = BTreeSet::new();
1007 let mut next_id = self.id;
1008
1009 loop {
1010 if visited.contains(&next_id) {
1011 panic!("BUG: cyclic dependency");
1012 }
1013 visited.insert(next_id);
1014
1015 match resolved_arguments.get(&next_id).unwrap() {
1016 ResolvedArgument::Unresolved => return Err(Ok(next_id)),
1017 ResolvedArgument::ReplaceWith(argument) => {
1018 next_id = argument.id;
1019 sub_index = argument.sub_index;
1020 }
1021 ResolvedArgument::Resolved(argument) => break argument,
1022 }
1023 }
1024 };
1025
1026 if let Some(sub_index) = sub_index {
1027 if let Some(arg) = arg.nested(sub_index as u16) {
1028 return Ok(arg);
1029 } else {
1030 return Err(Err(Error::Input(
1031 "unable to create nested argument".to_owned(),
1032 )));
1033 }
1034 }
1035
1036 Ok(*arg)
1037 }
1038
1039 fn try_resolve_many(
1040 arguments: &[Self],
1041 resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1042 ) -> Result<Vec<sui_sdk_types::Argument>, Result<usize, Error>> {
1043 arguments
1044 .iter()
1045 .map(|a| a.try_resolve(resolved_arguments))
1046 .collect::<Result<_, _>>()
1047 }
1048}
1049
1050pub(crate) struct Command {
1051 kind: CommandKind,
1052 pub(crate) dependencies: Vec<Argument>,
1055}
1056
1057impl From<CommandKind> for Command {
1058 fn from(value: CommandKind) -> Self {
1059 Self {
1060 kind: value,
1061 dependencies: Vec::new(),
1062 }
1063 }
1064}
1065
1066pub(crate) enum CommandKind {
1067 MoveCall(MoveCall),
1069
1070 TransferObjects(TransferObjects),
1075
1076 SplitCoins(SplitCoins),
1079
1080 MergeCoins(MergeCoins),
1083
1084 Publish(Publish),
1087
1088 MakeMoveVector(MakeMoveVector),
1092
1093 Upgrade(Upgrade),
1101}
1102
1103impl Command {
1104 fn try_resolve(
1105 &self,
1106 resolved_arguments: &BTreeMap<usize, ResolvedArgument>,
1107 ) -> Result<sui_sdk_types::Command, Result<usize, Error>> {
1108 use sui_sdk_types::Command as C;
1109
1110 Argument::try_resolve_many(&self.dependencies, resolved_arguments)?;
1112
1113 let cmd = match &self.kind {
1114 CommandKind::MoveCall(MoveCall {
1115 package,
1116 module,
1117 function,
1118 type_arguments,
1119 arguments,
1120 }) => C::MoveCall(sui_sdk_types::MoveCall {
1121 package: *package,
1122 module: module.to_owned(),
1123 function: function.to_owned(),
1124 type_arguments: type_arguments.to_owned(),
1125 arguments: Argument::try_resolve_many(arguments, resolved_arguments)?,
1126 }),
1127
1128 CommandKind::TransferObjects(TransferObjects { objects, address }) => {
1129 C::TransferObjects(sui_sdk_types::TransferObjects {
1130 objects: Argument::try_resolve_many(objects, resolved_arguments)?,
1131 address: address.try_resolve(resolved_arguments)?,
1132 })
1133 }
1134
1135 CommandKind::SplitCoins(SplitCoins { coin, amounts }) => {
1136 C::SplitCoins(sui_sdk_types::SplitCoins {
1137 coin: coin.try_resolve(resolved_arguments)?,
1138 amounts: Argument::try_resolve_many(amounts, resolved_arguments)?,
1139 })
1140 }
1141
1142 CommandKind::MergeCoins(MergeCoins {
1143 coin,
1144 coins_to_merge,
1145 }) => C::MergeCoins(sui_sdk_types::MergeCoins {
1146 coin: coin.try_resolve(resolved_arguments)?,
1147 coins_to_merge: Argument::try_resolve_many(coins_to_merge, resolved_arguments)?,
1148 }),
1149
1150 CommandKind::Publish(Publish {
1151 modules,
1152 dependencies,
1153 }) => C::Publish(sui_sdk_types::Publish {
1154 modules: modules.to_owned(),
1155 dependencies: dependencies.to_owned(),
1156 }),
1157
1158 CommandKind::MakeMoveVector(MakeMoveVector { type_, elements }) => {
1159 C::MakeMoveVector(sui_sdk_types::MakeMoveVector {
1160 type_: type_.to_owned(),
1161 elements: Argument::try_resolve_many(elements, resolved_arguments)?,
1162 })
1163 }
1164
1165 CommandKind::Upgrade(Upgrade {
1166 modules,
1167 dependencies,
1168 package,
1169 ticket,
1170 }) => C::Upgrade(sui_sdk_types::Upgrade {
1171 modules: modules.to_owned(),
1172 dependencies: dependencies.to_owned(),
1173 package: *package,
1174 ticket: ticket.try_resolve(resolved_arguments)?,
1175 }),
1176 };
1177 Ok(cmd)
1178 }
1179}
1180
1181pub(crate) struct TransferObjects {
1182 pub objects: Vec<Argument>,
1184
1185 pub address: Argument,
1187}
1188
1189pub(crate) struct SplitCoins {
1190 pub coin: Argument,
1192
1193 pub amounts: Vec<Argument>,
1195}
1196
1197pub(crate) struct MergeCoins {
1198 pub coin: Argument,
1200
1201 pub coins_to_merge: Vec<Argument>,
1205}
1206
1207pub(crate) struct Publish {
1208 pub modules: Vec<Vec<u8>>,
1210
1211 pub dependencies: Vec<Address>,
1213}
1214
1215pub(crate) struct MakeMoveVector {
1216 pub type_: Option<TypeTag>,
1221
1222 pub elements: Vec<Argument>,
1224}
1225
1226pub(crate) struct Upgrade {
1227 pub modules: Vec<Vec<u8>>,
1229
1230 pub dependencies: Vec<Address>,
1232
1233 pub package: Address,
1235
1236 pub ticket: Argument,
1238}
1239
1240pub(crate) struct MoveCall {
1241 pub package: Address,
1243
1244 pub module: Identifier,
1246
1247 pub function: Identifier,
1249
1250 pub type_arguments: Vec<TypeTag>,
1252
1253 pub arguments: Vec<Argument>,
1255 }
1257
1258#[derive(Clone)]
1280pub struct ObjectInput {
1281 object_id: Address,
1282 kind: Option<ObjectKind>,
1283 version: Option<u64>,
1284 digest: Option<Digest>,
1285 mutable: Option<bool>,
1286}
1287
1288#[derive(Clone, Copy)]
1289enum ObjectKind {
1290 Shared,
1291 Receiving,
1292 ImmutableOrOwned,
1293}
1294
1295impl ObjectInput {
1296 pub fn new(object_id: Address) -> Self {
1301 Self {
1302 kind: None,
1303 object_id,
1304 version: None,
1305 digest: None,
1306 mutable: None,
1307 }
1308 }
1309
1310 pub fn owned(object_id: Address, version: u64, digest: Digest) -> Self {
1312 Self {
1313 kind: Some(ObjectKind::ImmutableOrOwned),
1314 object_id,
1315 version: Some(version),
1316 digest: Some(digest),
1317 mutable: None,
1318 }
1319 }
1320
1321 pub fn immutable(object_id: Address, version: u64, digest: Digest) -> Self {
1323 Self {
1324 kind: Some(ObjectKind::ImmutableOrOwned),
1325 object_id,
1326 version: Some(version),
1327 digest: Some(digest),
1328 mutable: None,
1329 }
1330 }
1331
1332 pub fn receiving(object_id: Address, version: u64, digest: Digest) -> Self {
1334 Self {
1335 kind: Some(ObjectKind::Receiving),
1336 object_id,
1337 version: Some(version),
1338 digest: Some(digest),
1339 mutable: None,
1340 }
1341 }
1342
1343 pub fn shared(object_id: Address, version: u64, mutable: bool) -> Self {
1347 Self {
1348 kind: Some(ObjectKind::Shared),
1349 object_id,
1350 version: Some(version),
1351 mutable: Some(mutable),
1352 digest: None,
1353 }
1354 }
1355
1356 pub fn as_immutable(self) -> Self {
1358 Self {
1359 kind: Some(ObjectKind::ImmutableOrOwned),
1360 ..self
1361 }
1362 }
1363
1364 pub fn as_owned(self) -> Self {
1366 Self {
1367 kind: Some(ObjectKind::ImmutableOrOwned),
1368 ..self
1369 }
1370 }
1371
1372 pub fn as_receiving(self) -> Self {
1374 Self {
1375 kind: Some(ObjectKind::Receiving),
1376 ..self
1377 }
1378 }
1379
1380 pub fn as_shared(self) -> Self {
1382 Self {
1383 kind: Some(ObjectKind::Shared),
1384 ..self
1385 }
1386 }
1387
1388 pub fn with_version(self, version: u64) -> Self {
1390 Self {
1391 version: Some(version),
1392 ..self
1393 }
1394 }
1395
1396 pub fn with_digest(self, digest: Digest) -> Self {
1398 Self {
1399 digest: Some(digest),
1400 ..self
1401 }
1402 }
1403
1404 pub fn with_mutable(self, mutable: bool) -> Self {
1409 Self {
1410 mutable: Some(mutable),
1411 ..self
1412 }
1413 }
1414
1415 #[cfg(feature = "intents")]
1416 pub(crate) fn object_id(&self) -> Address {
1417 self.object_id
1418 }
1419}
1420
1421impl From<&sui_sdk_types::Object> for ObjectInput {
1422 fn from(object: &sui_sdk_types::Object) -> Self {
1423 let input = Self::new(object.object_id())
1424 .with_version(object.version())
1425 .with_digest(object.digest());
1426
1427 match object.owner() {
1428 sui_sdk_types::Owner::Address(_) => input.as_owned(),
1429 sui_sdk_types::Owner::Object(_) => input,
1430 sui_sdk_types::Owner::Shared(version) => input.with_version(*version).as_shared(),
1431 sui_sdk_types::Owner::Immutable => input.as_immutable(),
1432 sui_sdk_types::Owner::ConsensusAddress { start_version, .. } => {
1433 input.with_version(*start_version).as_shared()
1434 }
1435 _ => input,
1436 }
1437 }
1438}
1439
1440impl ObjectInput {
1442 fn try_into_object_reference(&self) -> Result<sui_sdk_types::ObjectReference, Error> {
1443 if matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None)
1444 && let Some(version) = self.version
1445 && let Some(digest) = self.digest
1446 {
1447 Ok(sui_sdk_types::ObjectReference::new(
1448 self.object_id,
1449 version,
1450 digest,
1451 ))
1452 } else {
1453 Err(Error::WrongGasObject)
1454 }
1455 }
1456
1457 fn try_into_input(&self) -> Result<sui_sdk_types::Input, Error> {
1458 let input = match self {
1459 Self {
1461 object_id,
1462 kind: Some(ObjectKind::ImmutableOrOwned),
1463 version: Some(version),
1464 digest: Some(digest),
1465 ..
1466 }
1467 | Self {
1468 object_id,
1469 kind: None,
1470 version: Some(version),
1471 digest: Some(digest),
1472 mutable: None,
1473 } => sui_sdk_types::Input::ImmutableOrOwned(sui_sdk_types::ObjectReference::new(
1474 *object_id, *version, *digest,
1475 )),
1476
1477 Self {
1479 object_id,
1480 kind: Some(ObjectKind::Receiving),
1481 version: Some(version),
1482 digest: Some(digest),
1483 ..
1484 } => sui_sdk_types::Input::Receiving(sui_sdk_types::ObjectReference::new(
1485 *object_id, *version, *digest,
1486 )),
1487
1488 Self {
1490 object_id,
1491 kind: Some(ObjectKind::Shared),
1492 version: Some(version),
1493 mutable: Some(mutable),
1494 ..
1495 }
1496 | Self {
1497 object_id,
1498 kind: None,
1499 version: Some(version),
1500 digest: None,
1501 mutable: Some(mutable),
1502 } => sui_sdk_types::Input::Shared(sui_sdk_types::SharedInput::new(
1503 *object_id, *version, *mutable,
1504 )),
1505
1506 _ => {
1507 return Err(Error::Input(format!(
1508 "Input object {} is incomplete",
1509 self.object_id
1510 )));
1511 }
1512 };
1513 Ok(input)
1514 }
1515
1516 #[cfg(feature = "intents")]
1517 fn to_input_proto(&self) -> sui_rpc::proto::sui::rpc::v2::Input {
1518 use sui_rpc::proto::sui::rpc::v2::input::InputKind;
1519
1520 let mut input =
1521 sui_rpc::proto::sui::rpc::v2::Input::default().with_object_id(self.object_id);
1522 match &self.kind {
1523 Some(ObjectKind::Shared) => input.set_kind(InputKind::Shared),
1524 Some(ObjectKind::Receiving) => input.set_kind(InputKind::Receiving),
1525 Some(ObjectKind::ImmutableOrOwned) => input.set_kind(InputKind::ImmutableOrOwned),
1526 None => {}
1527 }
1528
1529 if let Some(version) = self.version {
1530 input.set_version(version);
1531 }
1532
1533 if let Some(digest) = self.digest {
1534 input.set_digest(digest);
1535 }
1536
1537 if let Some(mutable) = self.mutable {
1538 input.set_mutable(mutable);
1539 }
1540
1541 input
1542 }
1543
1544 #[cfg(feature = "intents")]
1545 fn try_into_object_reference_proto(
1546 &self,
1547 ) -> Result<sui_rpc::proto::sui::rpc::v2::ObjectReference, Error> {
1548 if !matches!(self.kind, Some(ObjectKind::ImmutableOrOwned) | None) {
1549 return Err(Error::WrongGasObject);
1550 }
1551
1552 let mut input =
1553 sui_rpc::proto::sui::rpc::v2::ObjectReference::default().with_object_id(self.object_id);
1554 if let Some(version) = self.version {
1555 input.set_version(version);
1556 }
1557 if let Some(digest) = self.digest {
1558 input.set_digest(digest);
1559 }
1560 Ok(input)
1561 }
1562
1563 #[cfg(feature = "intents")]
1564 pub(crate) fn try_from_object_proto(
1565 object: &sui_rpc::proto::sui::rpc::v2::Object,
1566 ) -> Result<Self, Error> {
1567 use sui_rpc::proto::sui::rpc::v2::owner::OwnerKind;
1568
1569 let input = Self::new(
1570 object
1571 .object_id()
1572 .parse()
1573 .map_err(|_e| Error::MissingObjectId)?,
1574 );
1575
1576 Ok(match object.owner().kind() {
1577 OwnerKind::Address | OwnerKind::Immutable => {
1578 input.as_owned().with_version(object.version()).with_digest(
1579 object
1580 .digest()
1581 .parse()
1582 .map_err(|_| Error::Input("can't parse digest".to_owned()))?,
1583 )
1584 }
1585 OwnerKind::Object => return Err(Error::Input("invalid object type".to_owned())),
1586 OwnerKind::Shared | OwnerKind::ConsensusAddress => input
1587 .as_shared()
1588 .with_version(object.owner().version())
1589 .with_mutable(true),
1590 OwnerKind::Unknown | _ => input,
1591 })
1592 }
1593}
1594
1595pub struct Function {
1614 package: Address,
1616 module: Identifier,
1618 function: Identifier,
1620 type_args: Vec<TypeTag>,
1622}
1623
1624impl Function {
1625 pub fn new(package: Address, module: Identifier, function: Identifier) -> Self {
1639 Self {
1640 package,
1641 module,
1642 function,
1643 type_args: Vec::new(),
1644 }
1645 }
1646
1647 pub fn with_type_args(self, type_args: Vec<TypeTag>) -> Self {
1662 Self { type_args, ..self }
1663 }
1664}
1665
1666#[cfg(test)]
1667mod tests {
1668 use super::*;
1669
1670 #[test]
1671 fn simple_try_build() {
1672 let mut tx = TransactionBuilder::new();
1673 let _coin = tx.object(ObjectInput::owned(
1674 Address::from_static(
1675 "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1676 ),
1677 2,
1678 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1679 ));
1680 let _gas = tx.gas();
1681
1682 let _recipient = tx.pure(&Address::from_static("0xabc"));
1683
1684 assert!(tx.try_build().is_err());
1685
1686 let mut tx = TransactionBuilder::new();
1687 let coin = tx.object(ObjectInput::owned(
1688 Address::from_static(
1689 "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1690 ),
1691 2,
1692 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1693 ));
1694 let gas = tx.gas();
1695
1696 let recipient = tx.pure(&Address::from_static("0xabc"));
1697 tx.transfer_objects(vec![coin, gas], recipient);
1698 tx.set_gas_budget(500000000);
1699 tx.set_gas_price(1000);
1700 tx.add_gas_objects([ObjectInput::owned(
1701 Address::from_static(
1702 "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1703 ),
1704 2,
1705 Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1706 )]);
1707 tx.set_sender(Address::from_static(
1708 "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1709 ));
1710
1711 assert!(tx.try_build().is_ok());
1712 }
1713
1714 #[test]
1715 fn test_split_transfer() {
1716 let mut tx = TransactionBuilder::new();
1717
1718 let amount = tx.pure(&1_000_000_000u64);
1720 let gas = tx.gas();
1721 let result = tx.split_coins(gas, vec![amount; 5]);
1722 let recipient = tx.pure(&Address::from_static("0xabc"));
1723 tx.transfer_objects(result, recipient);
1724
1725 tx.set_gas_budget(500000000);
1726 tx.set_gas_price(1000);
1727 tx.add_gas_objects([ObjectInput::owned(
1728 Address::from_static(
1729 "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1730 ),
1731 2,
1732 Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1733 )]);
1734 tx.set_sender(Address::from_static(
1735 "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1736 ));
1737
1738 assert!(tx.try_build().is_ok());
1739 }
1740
1741 #[test]
1742 fn test_deterministic_building() {
1743 let build_tx = || {
1744 let mut tx = TransactionBuilder::new();
1745 let coin = tx.object(ObjectInput::owned(
1746 Address::from_static(
1747 "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4",
1748 ),
1749 2,
1750 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1751 ));
1752 let _ = tx.object(ObjectInput::owned(
1753 Address::from_static("0x12345"),
1754 2,
1755 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1756 ));
1757 let _ = tx.object(ObjectInput::owned(
1758 Address::from_static("0x12345"),
1759 2,
1760 Digest::from_static("7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9"),
1761 ));
1762 let gas = tx.gas();
1763 let _ = tx.pure(&Address::from_static("0xabc"));
1764 let _ = tx.pure(&Address::from_static("0xabc"));
1765 let _ = tx.pure(&Address::from_static("0xabc"));
1766 let _ = tx.pure(&Address::from_static("0xdef"));
1767 let _ = tx.pure(&1u64);
1768 let _ = tx.pure(&1u64);
1769 let _ = tx.pure(&1u64);
1770 let _ = tx.pure(&Some(2u8));
1771 let _ = tx.pure_unique(&Address::from_static("0xabc"));
1772 let _ = tx.pure_unique(&Address::from_static("0xabc"));
1773 let _ = tx.pure_unique(&1u64);
1774
1775 let recipient = tx.pure(&Address::from_static("0x123"));
1776 tx.transfer_objects(vec![coin, gas], recipient);
1777 tx.set_gas_budget(500000000);
1778 tx.set_gas_price(1000);
1779 tx.add_gas_objects([ObjectInput::owned(
1780 Address::from_static(
1781 "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821",
1782 ),
1783 2,
1784 Digest::from_static("2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"),
1785 )]);
1786 tx.set_sender(Address::from_static(
1787 "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793",
1788 ));
1789
1790 tx.try_build().unwrap()
1791 };
1792
1793 let digest = build_tx().digest();
1794
1795 assert!(
1796 (0..100)
1797 .map(|_| build_tx())
1798 .map(|tx| tx.digest())
1799 .all(|d| d == digest)
1800 )
1801 }
1802}