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    ///
219    ///  Examples:
220    ///  ### Upgrade a package with some pre-known data.
221    ///  ```rust,ignore
222    ///  use sui_graphql_client::Client;
223    ///  use sui_sdk_types::unresolved;
224    ///  use sui_transaction_builder::TransactionBuilder;
225    ///  use sui_transaction_builder::Function;
226    ///
227    ///  let mut tx = TransactionBuilder::new();
228    ///  let package_id = "0x...".parse().unwrap();
229    ///  let upgrade_cap = tx.input(unresolved::Input::by_id("0x...".parse().unwrap()));
230    ///  let upgrade_policy = tx.input(Serialized(&0u8));
231    ///  // the digest of the new package that was compiled
232    ///  let package_digest: &[u8] = &[
233    ///       68, 89, 156, 51, 190, 35, 155, 216, 248, 49, 135, 170, 106, 42, 190, 4, 208, 59, 155,
234    ///       89, 74, 63, 70, 95, 207, 78, 227, 22, 136, 146, 57, 79,
235    ///  ];
236    ///  let digest_arg = tx.input(Serialized(&package_digest));
237    ///
238    ///  // we need this ticket to authorize the upgrade
239    ///  let upgrade_ticket = tx.move_call(
240    ///      Function::new(
241    ///        "0x2".parse().unwrap(),
242    ///         "package".parse().unwrap(),
243    ///         "authorize_upgrade".parse().unwrap(),
244    ///         vec![],
245    ///      ),
246    ///      vec![upgrade_cap, upgrade_policy, digest_arg],
247    ///    );
248    ///  // now we can upgrade the package
249    ///  let upgrade_receipt = tx.upgrade(
250    ///       updated_modules,
251    ///       deps,
252    ///       package_id,
253    ///       upgrade_ticket,
254    ///  );
255    ///
256    ///  // commit the upgrade
257    ///  tx.move_call(
258    ///       Function::new(
259    ///          "0x2".parse().unwrap(),
260    ///          "package".parse().unwrap(),
261    ///          "commit_upgrade".parse().unwrap(),
262    ///          vec![],
263    ///      ),
264    ///      vec![upgrade_cap, upgrade_receipt],
265    ///  );
266    ///
267    ///  let client = Client::new_mainnet();
268    ///  let tx = tx.resolve(&client)?;
269    ///  ```
270    pub fn upgrade(
271        &mut self,
272        modules: Vec<Vec<u8>>,
273        dependencies: Vec<Address>,
274        package: Address,
275        ticket: Argument,
276    ) -> Argument {
277        let cmd = Command::Upgrade(Upgrade {
278            modules,
279            dependencies,
280            package,
281            ticket,
282        });
283        self.commands.push(cmd);
284        Argument::Result(self.commands.len() as u16 - 1)
285    }
286
287    /// Assuming everything is resolved, convert this transaction into the
288    /// resolved form. Returns a [`Transaction`] if successful, or an `Error` if not.
289    pub fn finish(self) -> Result<Transaction, Error> {
290        let Some(sender) = self.sender else {
291            return Err(Error::MissingSender);
292        };
293        if self.gas.is_empty() {
294            return Err(Error::MissingGasObjects);
295        }
296        let Some(budget) = self.gas_budget else {
297            return Err(Error::MissingGasBudget);
298        };
299        let Some(price) = self.gas_price else {
300            return Err(Error::MissingGasPrice);
301        };
302
303        Ok(Transaction {
304            kind: sui_types::TransactionKind::ProgrammableTransaction(
305                sui_types::ProgrammableTransaction {
306                    inputs: self
307                        .inputs
308                        .into_iter()
309                        .map(try_from_unresolved_input_arg)
310                        .collect::<Result<Vec<_>, _>>()?,
311                    commands: self.commands,
312                },
313            ),
314            sender,
315            gas_payment: {
316                GasPayment {
317                    objects: self
318                        .gas
319                        .into_iter()
320                        .map(try_from_gas_unresolved_input_to_unresolved_obj_ref)
321                        .collect::<Result<Vec<_>, _>>()?
322                        .into_iter()
323                        .map(try_from_unresolved_obj_ref)
324                        .collect::<Result<Vec<_>, _>>()?,
325                    owner: self.sponsor.unwrap_or(sender),
326                    price,
327                    budget,
328                }
329            },
330            expiration: self.expiration,
331        })
332    }
333}
334
335impl Function {
336    /// Constructor for the function type.
337    pub fn new(
338        package: Address,
339        module: Identifier,
340        function: Identifier,
341        type_args: Vec<TypeTag>,
342    ) -> Self {
343        Self {
344            package,
345            module,
346            function,
347            type_args,
348        }
349    }
350}
351
352impl From<RawBytes> for unresolved::Input {
353    fn from(raw: RawBytes) -> Self {
354        Self {
355            kind: Some(unresolved::InputKind::Pure),
356            value: Some(unresolved::Value::String(base64ct::Base64::encode_string(
357                &raw.0,
358            ))),
359            object_id: None,
360            version: None,
361            digest: None,
362            mutable: None,
363        }
364    }
365}
366
367impl<'a, T: Serialize> From<Serialized<'a, T>> for unresolved::Input {
368    fn from(value: Serialized<'a, T>) -> Self {
369        Self::from(RawBytes(bcs::to_bytes(value.0).unwrap()))
370    }
371}
372
373/// Convert from an [`unresolved::Input`] to a [`unresolved::ObjectReference`]. This is used to
374/// convert gas objects into unresolved object references.
375fn try_from_gas_unresolved_input_to_unresolved_obj_ref(
376    input: unresolved::Input,
377) -> Result<unresolved::ObjectReference, Error> {
378    match input.kind {
379        Some(unresolved::InputKind::ImmutableOrOwned) => {
380            let object_id = input.object_id.ok_or(Error::MissingObjectId)?;
381            let version = input.version;
382            let digest = input.digest;
383            Ok(unresolved::ObjectReference {
384                object_id,
385                version,
386                digest,
387            })
388        }
389        _ => Err(Error::WrongGasObject),
390    }
391}
392
393/// Convert from an [`unresolved::ObjectReference`] to a [`ObjectReference`].
394fn try_from_unresolved_obj_ref(obj: unresolved::ObjectReference) -> Result<ObjectReference, Error> {
395    let obj_id = obj.object_id;
396    let version = obj.version.ok_or(Error::MissingVersion(obj_id))?;
397    let digest = obj.digest.ok_or(Error::MissingDigest(obj_id))?;
398    Ok(ObjectReference::new(obj_id, version, digest))
399}
400
401/// Convert from an [`unresolved::Input`] into an [`Input`] for resolving the
402/// transaction.
403fn try_from_unresolved_input_arg(value: unresolved::Input) -> Result<Input, Error> {
404    if let Some(kind) = value.kind {
405        match kind {
406            unresolved::InputKind::Pure => {
407                let Some(value) = value.value else {
408                    return Err(Error::MissingPureValue);
409                };
410
411                match value {
412                    unresolved::Value::String(v) => {
413                        let bytes = base64ct::Base64::decode_vec(&v).map_err(Error::Decoding)?;
414                        Ok(Input::Pure { value: bytes })
415                    }
416                    _ => Err(Error::Input(
417                        "expected a base64 string value for the Pure input argument".to_string(),
418                    )),
419                }
420            }
421            unresolved::InputKind::ImmutableOrOwned => {
422                let Some(object_id) = value.object_id else {
423                    return Err(Error::MissingObjectId);
424                };
425                let Some(version) = value.version else {
426                    return Err(Error::MissingVersion(object_id));
427                };
428                let Some(digest) = value.digest else {
429                    return Err(Error::MissingDigest(object_id));
430                };
431                Ok(Input::ImmutableOrOwned(ObjectReference::new(
432                    object_id, version, digest,
433                )))
434            }
435            unresolved::InputKind::Shared => {
436                let Some(object_id) = value.object_id else {
437                    return Err(Error::MissingObjectId);
438                };
439                let Some(initial_shared_version) = value.version else {
440                    return Err(Error::MissingInitialSharedVersion(object_id));
441                };
442                let Some(mutable) = value.mutable else {
443                    return Err(Error::SharedObjectMutability(object_id));
444                };
445
446                Ok(Input::Shared {
447                    object_id,
448                    initial_shared_version,
449                    mutable,
450                })
451            }
452            unresolved::InputKind::Receiving => {
453                let Some(object_id) = value.object_id else {
454                    return Err(Error::MissingObjectId);
455                };
456                let Some(version) = value.version else {
457                    return Err(Error::MissingVersion(object_id));
458                };
459                let Some(digest) = value.digest else {
460                    return Err(Error::MissingDigest(object_id));
461                };
462                Ok(Input::Receiving(ObjectReference::new(
463                    object_id, version, digest,
464                )))
465            }
466            unresolved::InputKind::Literal => Err(Error::UnsupportedLiteral),
467        }
468    } else {
469        Err(Error::Input(
470            "unresolved::Input must have a kind that is not None".to_string(),
471        ))
472    }
473}
474
475#[cfg(test)]
476mod tests {
477    use std::str::FromStr;
478
479    use anyhow::Context;
480    use base64ct::Encoding;
481    use serde::de;
482    use serde::Deserialize;
483    use serde::Deserializer;
484
485    use sui_crypto::ed25519::Ed25519PrivateKey;
486    use sui_crypto::SuiSigner;
487    use sui_graphql_client::faucet::FaucetClient;
488    use sui_graphql_client::Client;
489    use sui_graphql_client::PaginationFilter;
490
491    use sui_types::framework::Coin;
492    use sui_types::Address;
493    use sui_types::Digest;
494    use sui_types::ExecutionStatus;
495    use sui_types::IdOperation;
496    use sui_types::ObjectType;
497    use sui_types::TransactionEffects;
498    use sui_types::TypeTag;
499
500    use crate::unresolved::Input;
501    use crate::Function;
502    use crate::Serialized;
503    use crate::TransactionBuilder;
504
505    /// Type corresponding to the output of `sui move build --dump-bytecode-as-base64`
506    #[derive(serde::Deserialize, Debug)]
507    struct MovePackageData {
508        #[serde(deserialize_with = "bcs_from_str")]
509        modules: Vec<Vec<u8>>,
510        #[serde(deserialize_with = "deps_from_str")]
511        dependencies: Vec<Address>,
512        digest: Vec<u8>,
513    }
514
515    fn bcs_from_str<'de, D>(deserializer: D) -> Result<Vec<Vec<u8>>, D::Error>
516    where
517        D: Deserializer<'de>,
518    {
519        let bcs = Vec::<String>::deserialize(deserializer)?;
520        bcs.into_iter()
521            .map(|s| base64ct::Base64::decode_vec(&s).map_err(de::Error::custom))
522            .collect()
523    }
524
525    fn deps_from_str<'de, D>(deserializer: D) -> Result<Vec<Address>, D::Error>
526    where
527        D: Deserializer<'de>,
528    {
529        let deps = Vec::<String>::deserialize(deserializer)?;
530        deps.into_iter()
531            .map(|s| Address::from_str(&s).map_err(de::Error::custom))
532            .collect()
533    }
534
535    /// This is used to read the json file that contains the modules/deps/digest generated with sui
536    /// move build --dump-bytecode-as-base64 on the `test_example_v1 and test_example_v2` projects
537    /// in the tests directory.
538    /// The json files are generated automatically when running `make test-with-localnet` in the
539    /// root of the sui-transaction-builder crate.
540    fn move_package_data(file: &str) -> MovePackageData {
541        let data = std::fs::read_to_string(file)
542            .with_context(|| {
543                format!(
544                    "Failed to read {file}. \
545                    Run `make test-with-localnet` from the root of the repository that will \
546                    generate the right json files with the package data and then run the tests."
547                )
548            })
549            .unwrap();
550        serde_json::from_str(&data).unwrap()
551    }
552
553    /// Generate a random private key and its corresponding address
554    fn helper_address_pk() -> (Address, Ed25519PrivateKey) {
555        let pk = Ed25519PrivateKey::generate(rand::thread_rng());
556        let address = pk.public_key().derive_address();
557        (address, pk)
558    }
559
560    /// Helper to:
561    /// - generate a private key and its corresponding address
562    /// - set the sender for the tx to this newly created address
563    /// - set gas price
564    /// - set gas budget
565    /// - call faucet which returns 5 coin objects (by default)
566    /// - set the gas object (last coin from the list of the 5 objects returned by faucet)
567    /// - return the address, private key, and coins.
568    async fn helper_setup(
569        tx: &mut TransactionBuilder,
570        client: &Client,
571    ) -> (Address, Ed25519PrivateKey, Vec<Coin<'static>>) {
572        let (address, pk) = helper_address_pk();
573        let faucet = FaucetClient::local();
574        let faucet_resp = faucet.request(address).await.unwrap();
575        wait_for_tx(
576            client,
577            faucet_resp
578                .coins_sent
579                .unwrap()
580                .first()
581                .unwrap()
582                .transfer_tx_digest,
583        )
584        .await;
585
586        let coins = client
587            .coins(address, None, PaginationFilter::default())
588            .await
589            .unwrap();
590        let coins = coins.data();
591
592        let gas = coins.last().unwrap().id();
593        // TODO when we have tx resolution, we can just pass an ObjectId
594        let gas_obj: Input = (&client.object(*gas, None).await.unwrap().unwrap()).into();
595        tx.add_gas_objects(vec![gas_obj.with_owned_kind()]);
596        tx.set_gas_budget(500000000);
597        tx.set_gas_price(1000);
598        tx.set_sender(address);
599
600        (address, pk, coins.to_vec())
601    }
602
603    /// Wait for the transaction to be finalized and indexed. This queries the GraphQL server until
604    /// it retrieves the requested transaction.
605    async fn wait_for_tx(client: &Client, digest: Digest) {
606        while client.transaction(digest).await.unwrap().is_none() {
607            tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
608        }
609    }
610
611    /// Wait for the transaction to be finalized and indexed, and check the effects' to ensure the
612    /// transaction was successfully executed.
613    async fn wait_for_tx_and_check_effects_status_success(
614        client: &Client,
615        digest: Digest,
616        effects: Result<Option<TransactionEffects>, sui_graphql_client::error::Error>,
617    ) {
618        assert!(effects.is_ok(), "Execution failed. Effects: {effects:?}");
619        // wait for the transaction to be finalized
620        wait_for_tx(client, digest).await;
621        // check that it succeeded
622        let status = effects.unwrap();
623        let expected_status = ExecutionStatus::Success;
624        assert_eq!(&expected_status, status.as_ref().unwrap().status());
625    }
626
627    #[tokio::test]
628    async fn test_finish() {
629        let mut tx = TransactionBuilder::new();
630        let coin_obj_id = "0x19406ea4d9609cd9422b85e6bf2486908f790b778c757aff805241f3f609f9b4";
631        let coin_digest = "7opR9rFUYivSTqoJHvFb9p6p54THyHTatMG6id4JKZR9";
632        let coin_version = 2;
633        let coin = tx.input(Input::owned(
634            coin_obj_id.parse().unwrap(),
635            coin_version,
636            coin_digest.parse().unwrap(),
637        ));
638
639        let addr = Address::generate(rand::thread_rng());
640        let recipient = tx.input(Serialized(&addr));
641
642        let result = tx.clone().finish();
643        assert!(result.is_err());
644
645        tx.transfer_objects(vec![coin], recipient);
646        tx.set_gas_budget(500000000);
647        tx.set_gas_price(1000);
648        tx.add_gas_objects(vec![Input::immutable(
649            "0xd8792bce2743e002673752902c0e7348dfffd78638cb5367b0b85857bceb9821"
650                .parse()
651                .unwrap(),
652            2,
653            "2ZigdvsZn5BMeszscPQZq9z8ebnS2FpmAuRbAi9ednCk"
654                .parse()
655                .unwrap(),
656        )]);
657        tx.set_sender(
658            "0xc574ea804d9c1a27c886312e96c0e2c9cfd71923ebaeb3000d04b5e65fca2793"
659                .parse()
660                .unwrap(),
661        );
662
663        let tx = tx.finish();
664        assert!(tx.is_ok());
665    }
666
667    #[tokio::test]
668    async fn test_transfer_obj_execution() {
669        let mut tx = TransactionBuilder::new();
670        let client = Client::new_localhost();
671        let (_, pk, coins) = helper_setup(&mut tx, &client).await;
672
673        // get the object information from the client
674        let first = coins.first().unwrap().id();
675        let coin: Input = (&client.object(*first, None).await.unwrap().unwrap()).into();
676        let coin_input = tx.input(coin.with_owned_kind());
677        let recipient = Address::generate(rand::thread_rng());
678        let recipient_input = tx.input(Serialized(&recipient));
679        tx.transfer_objects(vec![coin_input], recipient_input);
680
681        let tx = tx.finish().unwrap();
682        let sig = pk.sign_transaction(&tx).unwrap();
683
684        let effects = client.execute_tx(vec![sig], &tx).await;
685        wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
686
687        // check that recipient has 1 coin
688        let recipient_coins = client
689            .coins(recipient, None, PaginationFilter::default())
690            .await
691            .unwrap();
692        assert_eq!(recipient_coins.data().len(), 1);
693    }
694
695    #[tokio::test]
696    async fn test_move_call() {
697        // Check that `0x1::option::is_none` move call works when passing `1`
698        let client = Client::new_localhost();
699        let mut tx = TransactionBuilder::new();
700        // set up the sender, gas object, gas budget, and gas price and return the pk to sign
701        let (_, pk, _) = helper_setup(&mut tx, &client).await;
702        let function = Function::new(
703            "0x1".parse().unwrap(),
704            "option".parse().unwrap(),
705            "is_none".parse().unwrap(),
706            vec![TypeTag::U64],
707        );
708        let input = tx.input(Serialized(&vec![1u64]));
709        tx.move_call(function, vec![input]);
710
711        let tx = tx.finish().unwrap();
712        let sig = pk.sign_transaction(&tx).unwrap();
713        let effects = client.execute_tx(vec![sig], &tx).await;
714        wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
715    }
716
717    #[tokio::test]
718    async fn test_split_transfer() {
719        let client = Client::new_localhost();
720        let mut tx = TransactionBuilder::new();
721        let (_, pk, _) = helper_setup(&mut tx, &client).await;
722
723        // transfer 1 SUI from Gas coin
724        let amount = tx.input(Serialized(&1_000_000_000u64));
725        let result = tx.split_coins(tx.gas(), vec![amount]);
726        let recipient_address = Address::generate(rand::thread_rng());
727        let recipient = tx.input(Serialized(&recipient_address));
728        tx.transfer_objects(vec![result], recipient);
729
730        let tx = tx.finish().unwrap();
731        let sig = pk.sign_transaction(&tx).unwrap();
732
733        let effects = client.execute_tx(vec![sig], &tx).await;
734        wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
735
736        // check that recipient has 1 coin
737        let recipient_coins = client
738            .coins(recipient_address, None, PaginationFilter::default())
739            .await
740            .unwrap();
741        assert_eq!(recipient_coins.data().len(), 1);
742    }
743
744    #[tokio::test]
745    async fn test_split_without_transfer_should_fail() {
746        let client = Client::new_localhost();
747        let mut tx = TransactionBuilder::new();
748        let (_, pk, coins) = helper_setup(&mut tx, &client).await;
749
750        let coin = coins.first().unwrap().id();
751        let coin_obj: Input = (&client.object(*coin, None).await.unwrap().unwrap()).into();
752        let coin_input = tx.input(coin_obj.with_owned_kind());
753
754        // transfer 1 SUI
755        let amount = tx.input(Serialized(&1_000_000_000u64));
756        tx.split_coins(coin_input, vec![amount]);
757
758        let tx = tx.finish().unwrap();
759        let sig = pk.sign_transaction(&tx).unwrap();
760
761        let effects = client.execute_tx(vec![sig], &tx).await;
762        assert!(effects.is_ok());
763
764        // wait for the transaction to be finalized
765        loop {
766            let tx_digest = client.transaction(tx.digest()).await.unwrap();
767            if tx_digest.is_some() {
768                break;
769            }
770        }
771        assert!(effects.is_ok());
772        let status = effects.unwrap();
773        let expected_status = ExecutionStatus::Success;
774        // The tx failed, so we expect Failure instead of Success
775        assert_ne!(&expected_status, status.as_ref().unwrap().status());
776    }
777
778    #[tokio::test]
779    async fn test_merge_coins() {
780        let client = Client::new_localhost();
781        let mut tx = TransactionBuilder::new();
782        let (address, pk, coins) = helper_setup(&mut tx, &client).await;
783
784        let coin1 = coins.first().unwrap().id();
785        let coin1_obj: Input = (&client.object(*coin1, None).await.unwrap().unwrap()).into();
786        let coin_to_merge = tx.input(coin1_obj.with_owned_kind());
787
788        let mut coins_to_merge = vec![];
789        // last coin is used for gas, first coin is the one we merge into
790        for c in coins[1..&coins.len() - 1].iter() {
791            let coin: Input = (&client.object(*c.id(), None).await.unwrap().unwrap()).into();
792            coins_to_merge.push(tx.input(coin.with_owned_kind()));
793        }
794
795        tx.merge_coins(coin_to_merge, coins_to_merge);
796        let tx = tx.finish().unwrap();
797        let sig = pk.sign_transaction(&tx).unwrap();
798
799        let effects = client.execute_tx(vec![sig], &tx).await;
800        wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
801
802        // check that there are two coins
803        let coins_after = client
804            .coins(address, None, PaginationFilter::default())
805            .await
806            .unwrap();
807        assert_eq!(coins_after.data().len(), 2);
808    }
809
810    #[tokio::test]
811    async fn test_make_move_vec() {
812        let client = Client::new_localhost();
813        let mut tx = TransactionBuilder::new();
814        let (_, pk, _) = helper_setup(&mut tx, &client).await;
815
816        let input = tx.input(Serialized(&1u64));
817        tx.make_move_vec(Some(TypeTag::U64), vec![input]);
818
819        let tx = tx.finish().unwrap();
820        let sig = pk.sign_transaction(&tx).unwrap();
821
822        let effects = client.execute_tx(vec![sig], &tx).await;
823        wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
824    }
825
826    #[tokio::test]
827    async fn test_publish() {
828        let client = Client::new_localhost();
829        let mut tx = TransactionBuilder::new();
830        let (address, pk, _) = helper_setup(&mut tx, &client).await;
831
832        let package = move_package_data("package_test_example_v1.json");
833        let sender = tx.input(Serialized(&address));
834        let upgrade_cap = tx.publish(package.modules, package.dependencies);
835        tx.transfer_objects(vec![upgrade_cap], sender);
836        let tx = tx.finish().unwrap();
837        let sig = pk.sign_transaction(&tx).unwrap();
838        let effects = client.execute_tx(vec![sig], &tx).await;
839        wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
840    }
841
842    #[tokio::test]
843    async fn test_upgrade() {
844        let client = Client::new_localhost();
845        let mut tx = TransactionBuilder::new();
846        let (address, pk, coins) = helper_setup(&mut tx, &client).await;
847
848        let package = move_package_data("package_test_example_v2.json");
849        let sender = tx.input(Serialized(&address));
850        let upgrade_cap = tx.publish(package.modules, package.dependencies);
851        tx.transfer_objects(vec![upgrade_cap], sender);
852        let tx = tx.finish().unwrap();
853        let sig = pk.sign_transaction(&tx).unwrap();
854        let effects = client.execute_tx(vec![sig], &tx).await;
855        let mut package_id: Option<Address> = None;
856        let mut created_objs = vec![];
857        if let Ok(Some(ref effects)) = effects {
858            match effects {
859                TransactionEffects::V2(e) => {
860                    for obj in e.changed_objects.clone() {
861                        if obj.id_operation == IdOperation::Created {
862                            let change = obj.output_state;
863                            match change {
864                                sui_types::ObjectOut::PackageWrite { .. } => {
865                                    package_id = Some(obj.object_id);
866                                }
867                                sui_types::ObjectOut::ObjectWrite { .. } => {
868                                    created_objs.push(obj.object_id);
869                                }
870                                _ => {}
871                            }
872                        }
873                    }
874                }
875                _ => panic!("Expected V2 effects"),
876            }
877        }
878        wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
879
880        let mut tx = TransactionBuilder::new();
881        let mut upgrade_cap = None;
882        for o in created_objs {
883            let obj = client.object(o, None).await.unwrap().unwrap();
884            match obj.object_type() {
885                ObjectType::Struct(x) if x.name.to_string() == "UpgradeCap" => {
886                    match obj.owner() {
887                        sui_types::Owner::Address(_) => {
888                            let obj: Input = (&obj).into();
889                            upgrade_cap = Some(tx.input(obj.with_owned_kind()))
890                        }
891                        sui_types::Owner::Shared(_) => {
892                            upgrade_cap = Some(tx.input(&obj))
893                        }
894                        // If the capability is owned by an object, then the module defining the owning
895                        // object gets to decide how the upgrade capability should be used.
896                        sui_types::Owner::Object(_) => {
897                            panic!("Upgrade capability controlled by object")
898                        }
899                        sui_types::Owner::Immutable => panic!("Upgrade capability is stored immutably and cannot be used for upgrades"),
900                        sui_types::Owner::ConsensusAddress { .. } => {
901                            upgrade_cap = Some(tx.input(&obj))
902                        }
903                        _ => panic!("unknwon owner"),
904                    };
905                    break;
906                }
907                _ => {}
908            };
909        }
910
911        let upgrade_policy = tx.input(Serialized(&0u8));
912        let updated_package = move_package_data("package_test_example_v2.json");
913        let digest_arg = tx.input(Serialized(&updated_package.digest));
914
915        // we need this ticket to authorize the upgrade
916        let upgrade_ticket = tx.move_call(
917            Function::new(
918                "0x2".parse().unwrap(),
919                "package".parse().unwrap(),
920                "authorize_upgrade".parse().unwrap(),
921                vec![],
922            ),
923            vec![upgrade_cap.unwrap(), upgrade_policy, digest_arg],
924        );
925        // now we can upgrade the package
926        let upgrade_receipt = tx.upgrade(
927            updated_package.modules,
928            updated_package.dependencies,
929            package_id.unwrap(),
930            upgrade_ticket,
931        );
932
933        // commit the upgrade
934        tx.move_call(
935            Function::new(
936                "0x2".parse().unwrap(),
937                "package".parse().unwrap(),
938                "commit_upgrade".parse().unwrap(),
939                vec![],
940            ),
941            vec![upgrade_cap.unwrap(), upgrade_receipt],
942        );
943
944        let gas = coins.last().unwrap().id();
945        let gas_obj: Input = (&client.object(*gas, None).await.unwrap().unwrap()).into();
946        tx.add_gas_objects(vec![gas_obj.with_owned_kind()]);
947        tx.set_gas_budget(500000000);
948        tx.set_gas_price(1000);
949        tx.set_sender(address);
950        let tx = tx.finish().unwrap();
951        let sig = pk.sign_transaction(&tx).unwrap();
952        let effects = client.execute_tx(vec![sig], &tx).await;
953        wait_for_tx_and_check_effects_status_success(&client, tx.digest(), effects).await;
954    }
955}