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