sui_transaction_builder/lib.rs
1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4mod error;
5pub mod unresolved;
6
7use error::Error;
8use sui_types::Address;
9use sui_types::Argument;
10use sui_types::Command;
11use sui_types::GasPayment;
12use sui_types::Identifier;
13use sui_types::Input;
14use sui_types::MakeMoveVector;
15use sui_types::MergeCoins;
16use sui_types::MoveCall;
17use sui_types::ObjectReference;
18use sui_types::Publish;
19use sui_types::SplitCoins;
20use sui_types::Transaction;
21use sui_types::TransactionExpiration;
22use sui_types::TransferObjects;
23use sui_types::TypeTag;
24use sui_types::Upgrade;
25
26use base64ct::Encoding;
27use serde::Serialize;
28
29/// A builder for creating transactions. Use `resolve` to finalize the transaction data.
30#[derive(Clone, Default, Debug)]
31pub struct TransactionBuilder {
32 /// The inputs to the transaction.
33 inputs: Vec<unresolved::Input>,
34 /// The list of commands in the transaction. A command is a single operation in a programmable
35 /// transaction.
36 commands: Vec<Command>,
37 /// The gas objects that will be used to pay for the transaction. The most common way is to
38 /// use [`unresolved::Input::owned`] function to create a gas object and use the [`add_gas`]
39 /// method to set the gas objects.
40 gas: Vec<unresolved::Input>,
41 /// The gas budget for the transaction.
42 gas_budget: Option<u64>,
43 /// The gas price for the transaction.
44 gas_price: Option<u64>,
45 /// The sender of the transaction.
46 sender: Option<Address>,
47 /// The sponsor of the transaction. If None, the sender is also the sponsor.
48 sponsor: Option<Address>,
49 /// The expiration of the transaction. The default value of this type is no expiration.
50 expiration: TransactionExpiration,
51}
52
53/// A transaction input that bypasses serialization. The input contents is already BCS serialized
54/// and is put verbatim into the transaction.
55struct RawBytes(Vec<u8>);
56
57/// A transaction input that will be serialized from BCS.
58pub struct Serialized<'a, T: Serialize>(pub &'a T);
59
60/// A separate type to support denoting a function by a more structured representation.
61pub struct Function {
62 /// The package that contains the module with the function.
63 package: Address,
64 /// The module that contains the function.
65 module: Identifier,
66 /// The function name.
67 function: Identifier,
68 /// The type arguments for the function.
69 type_args: Vec<TypeTag>,
70}
71
72/// A transaction builder to build transactions.
73impl TransactionBuilder {
74 /// Create a new transaction builder and initialize its elements to default.
75 pub fn new() -> Self {
76 Self::default()
77 }
78
79 // Transaction Inputs
80
81 /// Make a value available to the transaction as an input.
82 pub fn input(&mut self, i: impl Into<unresolved::Input>) -> Argument {
83 let input = i.into();
84 self.inputs.push(input);
85 Argument::Input((self.inputs.len() - 1) as u16)
86 }
87
88 /// Return the argument to be the gas object.
89 pub fn gas(&self) -> Argument {
90 Argument::Gas
91 }
92
93 // Metadata
94
95 /// Add one or more gas objects to use to pay for the transaction.
96 ///
97 /// Most commonly the gas can be passed as a reference to an owned/immutable `Object`,
98 /// or can created using one of the of the constructors of the [`unresolved::Input`] enum,
99 /// e.g., [`unresolved::Input::owned`].
100 pub fn add_gas_objects<O, I>(&mut self, gas: I)
101 where
102 O: Into<unresolved::Input>,
103 I: IntoIterator<Item = O>,
104 {
105 self.gas.extend(gas.into_iter().map(|x| x.into()));
106 }
107
108 /// Set the gas budget for the transaction.
109 pub fn set_gas_budget(&mut self, budget: u64) {
110 self.gas_budget = Some(budget);
111 }
112
113 /// Set the gas price for the transaction.
114 pub fn set_gas_price(&mut self, price: u64) {
115 self.gas_price = Some(price);
116 }
117
118 /// Set the sender of the transaction.
119 pub fn set_sender(&mut self, sender: Address) {
120 self.sender = Some(sender);
121 }
122
123 /// Set the sponsor of the transaction.
124 pub fn set_sponsor(&mut self, sponsor: Address) {
125 self.sponsor = Some(sponsor);
126 }
127
128 /// Set the expiration of the transaction to be a specific epoch.
129 pub fn set_expiration(&mut self, epoch: u64) {
130 self.expiration = TransactionExpiration::Epoch(epoch);
131 }
132
133 // Commands
134
135 /// Call a Move function with the given arguments.
136 ///
137 /// - `function` is a structured representation of a package::module::function argument,
138 /// optionally with type arguments.
139 ///
140 /// The return value is a result argument that can be used in subsequent commands.
141 /// If the move call returns multiple results, you can access them using the
142 /// [`Argument::nested`] method.
143 pub fn move_call(&mut self, function: Function, arguments: Vec<Argument>) -> Argument {
144 let cmd = Command::MoveCall(MoveCall {
145 package: function.package,
146 module: function.module,
147 function: function.function,
148 type_arguments: function.type_args,
149 arguments,
150 });
151 self.commands.push(cmd);
152 Argument::Result(self.commands.len() as u16 - 1)
153 }
154
155 /// Transfer a list of objects to the given address, without producing any result.
156 pub fn transfer_objects(&mut self, objects: Vec<Argument>, address: Argument) {
157 let cmd = Command::TransferObjects(TransferObjects { objects, address });
158 self.commands.push(cmd);
159 }
160
161 /// Split a coin by the provided amounts, returning multiple results (as many as there are
162 /// amounts). To access the results, use the [`Argument::nested`] method to access the desired
163 /// coin by its index.
164 pub fn split_coins(&mut self, coin: Argument, amounts: Vec<Argument>) -> Argument {
165 let cmd = Command::SplitCoins(SplitCoins { coin, amounts });
166 self.commands.push(cmd);
167 Argument::Result(self.commands.len() as u16 - 1)
168 }
169
170 /// Merge a list of coins into a single coin, without producing any result.
171 pub fn merge_coins(&mut self, coin: Argument, coins_to_merge: Vec<Argument>) {
172 let cmd = Command::MergeCoins(MergeCoins {
173 coin,
174 coins_to_merge,
175 });
176 self.commands.push(cmd);
177 }
178
179 /// Make a move vector from a list of elements. If the elements are not objects, or the vector
180 /// is empty, a type must be supplied.
181 /// It returns the Move vector as an argument, that can be used in subsequent commands.
182 pub fn make_move_vec(&mut self, type_: Option<TypeTag>, elements: Vec<Argument>) -> Argument {
183 let cmd = Command::MakeMoveVector(MakeMoveVector { type_, elements });
184 self.commands.push(cmd);
185 Argument::Result(self.commands.len() as u16 - 1)
186 }
187
188 /// Publish a list of modules with the given dependencies. The result is the
189 /// `0x2::package::UpgradeCap` Move type. Note that the upgrade capability needs to be handled
190 /// after this call:
191 /// - transfer it to the transaction sender or another address
192 /// - burn it
193 /// - wrap it for access control
194 /// - discard the it to make a package immutable
195 ///
196 /// The arguments required for this command are:
197 /// - `modules`: is the modules' bytecode to be published
198 /// - `dependencies`: is the list of IDs of the transitive dependencies of the package
199 pub fn publish(&mut self, modules: Vec<Vec<u8>>, dependencies: Vec<Address>) -> Argument {
200 let cmd = Command::Publish(Publish {
201 modules,
202 dependencies,
203 });
204 self.commands.push(cmd);
205 Argument::Result(self.commands.len() as u16 - 1)
206 }
207
208 /// Upgrade a Move package.
209 ///
210 /// - `modules`: is the modules' bytecode for the modules to be published
211 /// - `dependencies`: is the list of IDs of the transitive dependencies of the package to be
212 /// upgraded
213 /// - `package`: is the ID of the current package being upgraded
214 /// - `ticket`: is the upgrade ticket
215 ///
216 /// To get the ticket, you have to call the `0x2::package::authorize_upgrade` function,
217 /// and pass the package ID, the upgrade policy, and package digest.
218 pub fn upgrade(
219 &mut self,
220 modules: Vec<Vec<u8>>,
221 dependencies: Vec<Address>,
222 package: Address,
223 ticket: Argument,
224 ) -> Argument {
225 let cmd = Command::Upgrade(Upgrade {
226 modules,
227 dependencies,
228 package,
229 ticket,
230 });
231 self.commands.push(cmd);
232 Argument::Result(self.commands.len() as u16 - 1)
233 }
234
235 /// Assuming everything is resolved, convert this transaction into the
236 /// resolved form. Returns a [`Transaction`] if successful, or an `Error` if not.
237 pub fn finish(self) -> Result<Transaction, Error> {
238 let Some(sender) = self.sender else {
239 return Err(Error::MissingSender);
240 };
241 if self.gas.is_empty() {
242 return Err(Error::MissingGasObjects);
243 }
244 let Some(budget) = self.gas_budget else {
245 return Err(Error::MissingGasBudget);
246 };
247 let Some(price) = self.gas_price else {
248 return Err(Error::MissingGasPrice);
249 };
250
251 Ok(Transaction {
252 kind: sui_types::TransactionKind::ProgrammableTransaction(
253 sui_types::ProgrammableTransaction {
254 inputs: self
255 .inputs
256 .into_iter()
257 .map(try_from_unresolved_input_arg)
258 .collect::<Result<Vec<_>, _>>()?,
259 commands: self.commands,
260 },
261 ),
262 sender,
263 gas_payment: {
264 GasPayment {
265 objects: self
266 .gas
267 .into_iter()
268 .map(try_from_gas_unresolved_input_to_unresolved_obj_ref)
269 .collect::<Result<Vec<_>, _>>()?
270 .into_iter()
271 .map(try_from_unresolved_obj_ref)
272 .collect::<Result<Vec<_>, _>>()?,
273 owner: self.sponsor.unwrap_or(sender),
274 price,
275 budget,
276 }
277 },
278 expiration: self.expiration,
279 })
280 }
281}
282
283impl Function {
284 /// Constructor for the function type.
285 pub fn new(
286 package: Address,
287 module: Identifier,
288 function: Identifier,
289 type_args: Vec<TypeTag>,
290 ) -> Self {
291 Self {
292 package,
293 module,
294 function,
295 type_args,
296 }
297 }
298}
299
300impl From<RawBytes> for unresolved::Input {
301 fn from(raw: RawBytes) -> Self {
302 Self {
303 kind: Some(unresolved::InputKind::Pure),
304 value: Some(unresolved::Value::String(base64ct::Base64::encode_string(
305 &raw.0,
306 ))),
307 object_id: None,
308 version: None,
309 digest: None,
310 mutable: None,
311 }
312 }
313}
314
315impl<'a, T: Serialize> From<Serialized<'a, T>> for unresolved::Input {
316 fn from(value: Serialized<'a, T>) -> Self {
317 Self::from(RawBytes(bcs::to_bytes(value.0).unwrap()))
318 }
319}
320
321/// Convert from an [`unresolved::Input`] to a [`unresolved::ObjectReference`]. This is used to
322/// convert gas objects into unresolved object references.
323fn try_from_gas_unresolved_input_to_unresolved_obj_ref(
324 input: unresolved::Input,
325) -> Result<unresolved::ObjectReference, Error> {
326 match input.kind {
327 Some(unresolved::InputKind::ImmutableOrOwned) => {
328 let object_id = input.object_id.ok_or(Error::MissingObjectId)?;
329 let version = input.version;
330 let digest = input.digest;
331 Ok(unresolved::ObjectReference {
332 object_id,
333 version,
334 digest,
335 })
336 }
337 _ => Err(Error::WrongGasObject),
338 }
339}
340
341/// Convert from an [`unresolved::ObjectReference`] to a [`ObjectReference`].
342fn try_from_unresolved_obj_ref(obj: unresolved::ObjectReference) -> Result<ObjectReference, Error> {
343 let obj_id = obj.object_id;
344 let version = obj.version.ok_or(Error::MissingVersion(obj_id))?;
345 let digest = obj.digest.ok_or(Error::MissingDigest(obj_id))?;
346 Ok(ObjectReference::new(obj_id, version, digest))
347}
348
349/// Convert from an [`unresolved::Input`] into an [`Input`] for resolving the
350/// transaction.
351fn try_from_unresolved_input_arg(value: unresolved::Input) -> Result<Input, Error> {
352 if let Some(kind) = value.kind {
353 match kind {
354 unresolved::InputKind::Pure => {
355 let Some(value) = value.value else {
356 return Err(Error::MissingPureValue);
357 };
358
359 match value {
360 unresolved::Value::String(v) => {
361 let bytes = base64ct::Base64::decode_vec(&v).map_err(Error::Decoding)?;
362 Ok(Input::Pure { value: bytes })
363 }
364 _ => Err(Error::Input(
365 "expected a base64 string value for the Pure input argument".to_string(),
366 )),
367 }
368 }
369 unresolved::InputKind::ImmutableOrOwned => {
370 let Some(object_id) = value.object_id else {
371 return Err(Error::MissingObjectId);
372 };
373 let Some(version) = value.version else {
374 return Err(Error::MissingVersion(object_id));
375 };
376 let Some(digest) = value.digest else {
377 return Err(Error::MissingDigest(object_id));
378 };
379 Ok(Input::ImmutableOrOwned(ObjectReference::new(
380 object_id, version, digest,
381 )))
382 }
383 unresolved::InputKind::Shared => {
384 let Some(object_id) = value.object_id else {
385 return Err(Error::MissingObjectId);
386 };
387 let Some(initial_shared_version) = value.version else {
388 return Err(Error::MissingInitialSharedVersion(object_id));
389 };
390 let Some(mutable) = value.mutable else {
391 return Err(Error::SharedObjectMutability(object_id));
392 };
393
394 Ok(Input::Shared {
395 object_id,
396 initial_shared_version,
397 mutable,
398 })
399 }
400 unresolved::InputKind::Receiving => {
401 let Some(object_id) = value.object_id else {
402 return Err(Error::MissingObjectId);
403 };
404 let Some(version) = value.version else {
405 return Err(Error::MissingVersion(object_id));
406 };
407 let Some(digest) = value.digest else {
408 return Err(Error::MissingDigest(object_id));
409 };
410 Ok(Input::Receiving(ObjectReference::new(
411 object_id, version, digest,
412 )))
413 }
414 unresolved::InputKind::Literal => Err(Error::UnsupportedLiteral),
415 }
416 } else {
417 Err(Error::Input(
418 "unresolved::Input must have a kind that is not None".to_string(),
419 ))
420 }
421}
422
423// #[cfg(test)]
424// mod tests {
425// use std::str::FromStr;
426
427// use anyhow::Context;
428// use base64ct::Encoding;
429// use serde::de;
430// use serde::Deserialize;
431// use serde::Deserializer;
432
433// use sui_crypto::ed25519::Ed25519PrivateKey;
434// use sui_crypto::SuiSigner;
435// use sui_graphql_client::faucet::FaucetClient;
436// use sui_graphql_client::Client;
437// use sui_graphql_client::PaginationFilter;
438
439// use sui_types::framework::Coin;
440// use sui_types::Address;
441// use sui_types::Digest;
442// use sui_types::ExecutionStatus;
443// use sui_types::IdOperation;
444// use sui_types::ObjectType;
445// use sui_types::TransactionEffects;
446// use sui_types::TypeTag;
447
448// use crate::unresolved::Input;
449// use crate::Function;
450// use crate::Serialized;
451// use crate::TransactionBuilder;
452
453// /// Type corresponding to the output of `sui move build --dump-bytecode-as-base64`
454// #[derive(serde::Deserialize, Debug)]
455// struct MovePackageData {
456// #[serde(deserialize_with = "bcs_from_str")]
457// modules: Vec<Vec<u8>>,
458// #[serde(deserialize_with = "deps_from_str")]
459// dependencies: Vec<Address>,
460// digest: Vec<u8>,
461// }
462
463// fn bcs_from_str<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
464// where
465// D: Deserializer<'de>,
466// {
467// let bcs = Vec::<String>::deserialize(deserializer)?;
468// bcs.into_iter()
469// .map(|s| base64ct::Base64::decode_vec(&s).map_err(de::Error::custom))
470// .collect()
471// }
472
473// fn deps_from_str<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
474// where
475// D: Deserializer<'de>,
476// {
477// let deps = Vec::<String>::deserialize(deserializer)?;
478// deps.into_iter()
479// .map(|s| Address::from_str(&s).map_err(de::Error::custom))
480// .collect()
481// }
482
483// /// This is used to read the json file that contains the modules/deps/digest generated with sui
484// /// move build --dump-bytecode-as-base64 on the `test_example_v1 and test_example_v2` projects
485// /// in the tests directory.
486// /// The json files are generated automatically when running `make test-with-localnet` in the
487// /// root of the sui-transaction-builder crate.
488// fn move_package_data(file: &str) -> MovePackageData {
489// let data = std::fs::read_to_string(file)
490// .with_context(|| {
491// format!(
492// "Failed to read {file}. \
493// Run `make test-with-localnet` from the root of the repository that will \
494// generate the right json files with the package data and then run the tests."
495// )
496// })
497// .unwrap();
498// serde_json::from_str(&data).unwrap()
499// }
500
501// /// Generate a random private key and its corresponding address
502// fn helper_address_pk() -> (Address, Ed25519PrivateKey) {
503// let pk = Ed25519PrivateKey::generate(rand::thread_rng());
504// let address = pk.public_key().derive_address();
505// (address, pk)
506// }
507
508// /// Helper to:
509// /// - generate a private key and its corresponding address
510// /// - set the sender for the tx to this newly created address
511// /// - set gas price
512// /// - set gas budget
513// /// - call faucet which returns 5 coin objects (by default)
514// /// - set the gas object (last coin from the list of the 5 objects returned by faucet)
515// /// - return the address, private key, and coins.
516// async fn helper_setup(
517// tx: &mut TransactionBuilder,
518// client: &Client,
519// ) -> (Address, Ed25519PrivateKey, Vec<Coin<'static>>) {
520// let (address, pk) = helper_address_pk();
521// let faucet = FaucetClient::local();
522// let faucet_resp = faucet.request(address).await.unwrap();
523// wait_for_tx(
524// client,
525// faucet_resp
526// .coins_sent
527// .unwrap()
528// .first()
529// .unwrap()
530// .transfer_tx_digest,
531// )
532// .await;
533
534// let coins = client
535// .coins(address, None, PaginationFilter::default())
536// .await
537// .unwrap();
538// let coins = coins.data();
539
540// let gas = coins.last().unwrap().id();
541// // TODO when we have tx resolution, we can just pass an ObjectId
542// let gas_obj: Input = (&client.object(*gas, None).await.unwrap().unwrap()).into();
543// tx.add_gas_objects(vec![gas_obj.with_owned_kind()]);
544// tx.set_gas_budget(500000000);
545// tx.set_gas_price(1000);
546// tx.set_sender(address);
547
548// (address, pk, coins.to_vec())
549// }
550
551// /// Wait for the transaction to be finalized and indexed. This queries the GraphQL server until
552// /// it retrieves the requested transaction.
553// async fn wait_for_tx(client: &Client, digest: Digest) {
554// while client.transaction(digest).await.unwrap().is_none() {
555// tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
556// }
557// }
558
559// /// Wait for the transaction to be finalized and indexed, and check the effects' to ensure the
560// /// transaction was successfully executed.
561// async fn wait_for_tx_and_check_effects_status_success(
562// client: &Client,
563// digest: Digest,
564// effects: Result<Option<TransactionEffects>, sui_graphql_client::error::Error>,
565// ) {
566// assert!(effects.is_ok(), "Execution failed. Effects: {effects:?}");
567// // wait for the transaction to be finalized
568// wait_for_tx(client, digest).await;
569// // check that it succeeded
570// let status = effects.unwrap();
571// let expected_status = ExecutionStatus::Success;
572// assert_eq!(&expected_status, status.as_ref().unwrap().status());
573// }
574
575// #[tokio::test]
576// async fn test_finish() {
577// let mut tx = TransactionBuilder::new();
578// let coin_obj_id = "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4";
579// let coin_digest = "7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9";
580// let coin_version = 2;
581// let coin = tx.input(Input::owned(
582// coin_obj_id.parse().unwrap(),
583// coin_version,
584// coin_digest.parse().unwrap(),
585// ));
586
587// let addr = Address::generate(rand::thread_rng());
588// let recipient = tx.input(Serialized(&addr));
589
590// let result = tx.clone().finish();
591// assert!(result.is_err());
592
593// tx.transfer_objects(vec![coin], recipient);
594// tx.set_gas_budget(500000000);
595// tx.set_gas_price(1000);
596// tx.add_gas_objects(vec![Input::immutable(
597// "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821"
598// .parse()
599// .unwrap(),
600// 2,
601// "2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"
602// .parse()
603// .unwrap(),
604// )]);
605// tx.set_sender(
606// "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793"
607// .parse()
608// .unwrap(),
609// );
610
611// let tx = tx.finish();
612// assert!(tx.is_ok());
613// }
614
615// #[tokio::test]
616// async fn test_transfer_obj_execution() {
617// let mut tx = TransactionBuilder::new();
618// let client = Client::new_localhost();
619// let (_, pk, coins) = helper_setup(&mut tx, &client).await;
620
621// // get the object information from the client
622// let first = coins.first().unwrap().id();
623// let coin: Input = (&client.object(*first, None).await.unwrap().unwrap()).into();
624// let coin_input = tx.input(coin.with_owned_kind());
625// let recipient = Address::generate(rand::thread_rng());
626// let recipient_input = tx.input(Serialized(&recipient));
627// tx.transfer_objects(vec![coin_input], recipient_input);
628
629// let tx = tx.finish().unwrap();
630// let sig = pk.sign_transaction(&tx).unwrap();
631
632// let effects = client.execute_tx(vec![sig], &tx).await;
633// wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
634
635// // check that recipient has 1 coin
636// let recipient_coins = client
637// .coins(recipient, None, PaginationFilter::default())
638// .await
639// .unwrap();
640// assert_eq!(recipient_coins.data().len(), 1);
641// }
642
643// #[tokio::test]
644// async fn test_move_call() {
645// // Check that `0x1::option::is_none` move call works when passing `1`
646// let client = Client::new_localhost();
647// let mut tx = TransactionBuilder::new();
648// // set up the sender, gas object, gas budget, and gas price and return the pk to sign
649// let (_, pk, _) = helper_setup(&mut tx, &client).await;
650// let function = Function::new(
651// "0x1".parse().unwrap(),
652// "option".parse().unwrap(),
653// "is_none".parse().unwrap(),
654// vec![TypeTag::U64],
655// );
656// let input = tx.input(Serialized(&vec![1u64]));
657// tx.move_call(function, vec![input]);
658
659// let tx = tx.finish().unwrap();
660// let sig = pk.sign_transaction(&tx).unwrap();
661// let effects = client.execute_tx(vec![sig], &tx).await;
662// wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
663// }
664
665// #[tokio::test]
666// async fn test_split_transfer() {
667// let client = Client::new_localhost();
668// let mut tx = TransactionBuilder::new();
669// let (_, pk, _) = helper_setup(&mut tx, &client).await;
670
671// // transfer 1 SUI from Gas coin
672// let amount = tx.input(Serialized(&1_000_000_000u64));
673// let result = tx.split_coins(tx.gas(), vec![amount]);
674// let recipient_address = Address::generate(rand::thread_rng());
675// let recipient = tx.input(Serialized(&recipient_address));
676// tx.transfer_objects(vec![result], recipient);
677
678// let tx = tx.finish().unwrap();
679// let sig = pk.sign_transaction(&tx).unwrap();
680
681// let effects = client.execute_tx(vec![sig], &tx).await;
682// wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
683
684// // check that recipient has 1 coin
685// let recipient_coins = client
686// .coins(recipient_address, None, PaginationFilter::default())
687// .await
688// .unwrap();
689// assert_eq!(recipient_coins.data().len(), 1);
690// }
691
692// #[tokio::test]
693// async fn test_split_without_transfer_should_fail() {
694// let client = Client::new_localhost();
695// let mut tx = TransactionBuilder::new();
696// let (_, pk, coins) = helper_setup(&mut tx, &client).await;
697
698// let coin = coins.first().unwrap().id();
699// let coin_obj: Input = (&client.object(*coin, None).await.unwrap().unwrap()).into();
700// let coin_input = tx.input(coin_obj.with_owned_kind());
701
702// // transfer 1 SUI
703// let amount = tx.input(Serialized(&1_000_000_000u64));
704// tx.split_coins(coin_input, vec![amount]);
705
706// let tx = tx.finish().unwrap();
707// let sig = pk.sign_transaction(&tx).unwrap();
708
709// let effects = client.execute_tx(vec![sig], &tx).await;
710// assert!(effects.is_ok());
711
712// // wait for the transaction to be finalized
713// loop {
714// let tx_digest = client.transaction(tx.digest()).await.unwrap();
715// if tx_digest.is_some() {
716// break;
717// }
718// }
719// assert!(effects.is_ok());
720// let status = effects.unwrap();
721// let expected_status = ExecutionStatus::Success;
722// // The tx failed, so we expect Failure instead of Success
723// assert_ne!(&expected_status, status.as_ref().unwrap().status());
724// }
725
726// #[tokio::test]
727// async fn test_merge_coins() {
728// let client = Client::new_localhost();
729// let mut tx = TransactionBuilder::new();
730// let (address, pk, coins) = helper_setup(&mut tx, &client).await;
731
732// let coin1 = coins.first().unwrap().id();
733// let coin1_obj: Input = (&client.object(*coin1, None).await.unwrap().unwrap()).into();
734// let coin_to_merge = tx.input(coin1_obj.with_owned_kind());
735
736// let mut coins_to_merge = vec![];
737// // last coin is used for gas, first coin is the one we merge into
738// for c in coins[1..&coins.len() - 1].iter() {
739// let coin: Input = (&client.object(*c.id(), None).await.unwrap().unwrap()).into();
740// coins_to_merge.push(tx.input(coin.with_owned_kind()));
741// }
742
743// tx.merge_coins(coin_to_merge, coins_to_merge);
744// let tx = tx.finish().unwrap();
745// let sig = pk.sign_transaction(&tx).unwrap();
746
747// let effects = client.execute_tx(vec![sig], &tx).await;
748// wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
749
750// // check that there are two coins
751// let coins_after = client
752// .coins(address, None, PaginationFilter::default())
753// .await
754// .unwrap();
755// assert_eq!(coins_after.data().len(), 2);
756// }
757
758// #[tokio::test]
759// async fn test_make_move_vec() {
760// let client = Client::new_localhost();
761// let mut tx = TransactionBuilder::new();
762// let (_, pk, _) = helper_setup(&mut tx, &client).await;
763
764// let input = tx.input(Serialized(&1u64));
765// tx.make_move_vec(Some(TypeTag::U64), vec![input]);
766
767// let tx = tx.finish().unwrap();
768// let sig = pk.sign_transaction(&tx).unwrap();
769
770// let effects = client.execute_tx(vec![sig], &tx).await;
771// wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
772// }
773
774// #[tokio::test]
775// async fn test_publish() {
776// let client = Client::new_localhost();
777// let mut tx = TransactionBuilder::new();
778// let (address, pk, _) = helper_setup(&mut tx, &client).await;
779
780// let package = move_package_data("package_test_example_v1.json");
781// let sender = tx.input(Serialized(&address));
782// let upgrade_cap = tx.publish(package.modules, package.dependencies);
783// tx.transfer_objects(vec![upgrade_cap], sender);
784// let tx = tx.finish().unwrap();
785// let sig = pk.sign_transaction(&tx).unwrap();
786// let effects = client.execute_tx(vec![sig], &tx).await;
787// wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
788// }
789
790// #[tokio::test]
791// async fn test_upgrade() {
792// let client = Client::new_localhost();
793// let mut tx = TransactionBuilder::new();
794// let (address, pk, coins) = helper_setup(&mut tx, &client).await;
795
796// let package = move_package_data("package_test_example_v2.json");
797// let sender = tx.input(Serialized(&address));
798// let upgrade_cap = tx.publish(package.modules, package.dependencies);
799// tx.transfer_objects(vec![upgrade_cap], sender);
800// let tx = tx.finish().unwrap();
801// let sig = pk.sign_transaction(&tx).unwrap();
802// let effects = client.execute_tx(vec![sig], &tx).await;
803// let mut package_id: Option<Address> = None;
804// let mut created_objs = vec![];
805// if let Ok(Some(ref effects)) = effects {
806// match effects {
807// TransactionEffects::V2(e) => {
808// for obj in e.changed_objects.clone() {
809// if obj.id_operation == IdOperation::Created {
810// let change = obj.output_state;
811// match change {
812// sui_types::ObjectOut::PackageWrite { .. } => {
813// package_id = Some(obj.object_id);
814// }
815// sui_types::ObjectOut::ObjectWrite { .. } => {
816// created_objs.push(obj.object_id);
817// }
818// _ => {}
819// }
820// }
821// }
822// }
823// _ => panic!("Expected V2 effects"),
824// }
825// }
826// wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
827
828// let mut tx = TransactionBuilder::new();
829// let mut upgrade_cap = None;
830// for o in created_objs {
831// let obj = client.object(o, None).await.unwrap().unwrap();
832// match obj.object_type() {
833// ObjectType::Struct(x) if x.name.to_string() == "UpgradeCap" => {
834// match obj.owner() {
835// sui_types::Owner::Address(_) => {
836// let obj: Input = (&obj).into();
837// upgrade_cap = Some(tx.input(obj.with_owned_kind()))
838// }
839// sui_types::Owner::Shared(_) => {
840// upgrade_cap = Some(tx.input(&obj))
841// }
842// // If the capability is owned by an object, then the module defining the owning
843// // object gets to decide how the upgrade capability should be used.
844// sui_types::Owner::Object(_) => {
845// panic!("Upgrade capability controlled by object")
846// }
847// sui_types::Owner::Immutable => panic!("Upgrade capability is stored immutably and cannot be used for upgrades"),
848// sui_types::Owner::ConsensusAddress { .. } => {
849// upgrade_cap = Some(tx.input(&obj))
850// }
851// _ => panic!("unknwon owner"),
852// };
853// break;
854// }
855// _ => {}
856// };
857// }
858
859// let upgrade_policy = tx.input(Serialized(&0u8));
860// let updated_package = move_package_data("package_test_example_v2.json");
861// let digest_arg = tx.input(Serialized(&updated_package.digest));
862
863// // we need this ticket to authorize the upgrade
864// let upgrade_ticket = tx.move_call(
865// Function::new(
866// "0x2".parse().unwrap(),
867// "package".parse().unwrap(),
868// "authorize_upgrade".parse().unwrap(),
869// vec![],
870// ),
871// vec![upgrade_cap.unwrap(), upgrade_policy, digest_arg],
872// );
873// // now we can upgrade the package
874// let upgrade_receipt = tx.upgrade(
875// updated_package.modules,
876// updated_package.dependencies,
877// package_id.unwrap(),
878// upgrade_ticket,
879// );
880
881// // commit the upgrade
882// tx.move_call(
883// Function::new(
884// "0x2".parse().unwrap(),
885// "package".parse().unwrap(),
886// "commit_upgrade".parse().unwrap(),
887// vec![],
888// ),
889// vec![upgrade_cap.unwrap(), upgrade_receipt],
890// );
891
892// let gas = coins.last().unwrap().id();
893// let gas_obj: Input = (&client.object(*gas, None).await.unwrap().unwrap()).into();
894// tx.add_gas_objects(vec![gas_obj.with_owned_kind()]);
895// tx.set_gas_budget(500000000);
896// tx.set_gas_price(1000);
897// tx.set_sender(address);
898// let tx = tx.finish().unwrap();
899// let sig = pk.sign_transaction(&tx).unwrap();
900// let effects = client.execute_tx(vec![sig], &tx).await;
901// wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
902// }
903// }