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