Create and Execute PTBs on Sui with the Sui CLI

The new CLI command streamlines programming on Sui for those who prefer the terminal and Bash scripts.

Create and Execute PTBs on Sui with the Sui CLI

A new command in Sui's command line interface (CLI) lets users create and execute Programmable Transaction Blocks (PTBs) directly from the terminal or a Bash script. This new command gives developers greater flexibility in implementing and executing PTBs.

PTBs give developers a very powerful programming tool not available on other blockchains. The ability to chain multiple transactions together in a single executable may be taken for granted in the wider world of software engineering, but on blockchains it represents a fundamental leap forward.  

Until now, it was only possible to work with PTBs using one of the available SDKs. Now, developers who prefer working through the terminal and writing scripts have new means of using this powerful feature.

Check out the Sui CLI PTB command documentation for details and examples. 

The example below shows the commands necessary to create a simple PTB. It creates three new coins with values 1000, 2000 , and 3000 MIST from the gas coin, and transfers them to the address:  0x02a212de6a9dfa3a69e22387acfbafbb1a9e591bd9d636e7895dcfc8de05f331 (note the @ in front of the address to indicate this is an address and not a hexadecimal). 

sui client ptb \
--assign to_address @0x02a212de6a9dfa3a69e22387acfbafbb1a9e591bd \
--split-coins gas [1000,2000,3000] \
--assign coins \
--transfer-objects to_address [coins.0, coins.1, coins.2] \
--gas-budget 5000000 \
--summary

Here is the result message:

╭──────────────────────────────────────────────────────╮ 
│ PTB Execution Summary                                │ 
├──────────────────────────────────────────────────────┤ 
│ Digest: 7S7XpkGns8mQ4mBUzjdM32xqNHGF2P1ErHSLbb4Chn2j │ 
│ Status: success                                      │
│ Gas Cost Summary:                                    │ 
│ Storage Cost: 3952000                                │ 
│ Computation Cost: 1000000                            │ 
│ Storage Rebate: 978120                               │ 
│ Non-refundable Storage Fee: 9880                     │ 
╰──────────────────────────────────────────────────────╯

Sui CLI PTB 

The main philosophy behind CLI PTB support is to enable developers to build and execute PTBs from the command line. You can also write Bash scripts to construct and execute PTBs just as you would do from the command line, providing great flexibility when it comes to automating different tasks. 

The sui client ptb command accepts the following options:

sui client ptb -h
Build, preview, and execute programmable transaction blocks. Depending on your shell, you might have to use quotes around arrays or other passed values. Use --help to see examples for how to use the core functionality of this command.

Usage: sui client ptb [OPTIONS]

Options:
      --assign <NAME> <VALUE>                                         Assign a value to a variable name to use later in the PTB.
      --gas-coin <ID>                                                 The object ID of the gas coin to use. If not specified, it will try to use the first gas coin that it finds that has at least
                                                                      the requested gas-budget balance.
      --gas-budget <MIST>                                             The gas budget for the transaction, in MIST.
      --make-move-vec <TYPE> <[VALUES]>                               Given n-values of the same type, it constructs a vector. For non objects or an empty vector, the type tag must be specified.
      --merge-coins <INTO_COIN> <[COIN OBJECTS]>                      Merge N coins into the provided coin.
      --move-call <PACKAGE::MODULE::FUNCTION> <TYPE> <FUNCTION_ARGS>  Make a move call to a function.
      --split-coins <COIN> <[AMOUNT]>                                 Split the coin into N coins as per the given array of amounts.
      --transfer-objects <[OBJECTS]> <TO>                             Transfer objects to the specified address.
      --publish <MOVE_PACKAGE_PATH>                                   Publish the Move package. It takes as input the folder where the package exists.
      --upgrade <MOVE_PACKAGE_PATH>                                   Upgrade the move package. It takes as input the folder where the package exists.
      --preview                                                       Preview the list of PTB transactions instead of executing them.
      --summary                                                       Show only a short summary (digest, execution status, gas cost). Do not use this flag when you need all the transaction data
                                                                      and the execution effects.
      --warn-shadows                                                  Enable shadow warning when the same variable name is declared multiple times. Off by default.
      --json                                                          Return command outputs in json format.
  -h, --help                                                          Print help (see more with '--help')

Concepts and features 

Besides using existing traditional PTB-related concepts, there are a few new concepts needed for this command.  

  • Reuse results or custom defined variables using the --assign command. 
  • Preview the input commands in the PTB instead of executing the PTB. 
  • Warn on declaring a variable with the same identifier multiple times. 
  • Benefit from name resolution for common packages such as sui, std, and deepbook (instead of using their addresses).
  • Error messages provide a concise and precise explanation of what is wrong in the PTB syntax.
  • Variable names cannot be address, bool, vector, some, none, gas, u8, u16, u32, u64, u128, or u256.

Examples 

Let’s dig through some examples to showcase the Sui CLI PTB tool's power. Note that these examples were tested using Bash, and your mileage might vary depending on your shell and how it treats and parses strings and quotes (e.g., you might need to use quotation marks around arrays in zsh or fish, but not in Bash).

Nested or multi-types 

When you need to work with nested types, you can simply wrap the types you need in < and >. For example, if an argument expects an Option<vector<u256>> you can write std::option::Option<vector<u256>>.

Here is an example:

sui client ptb 
--make-move-vec "<std::option::Option<vector<u256>>>" "[vector[]]" \
--gas-budget 5000000

If you need to pass multiple types for a call, you can do so by using a comma between the types, as in <u64,u8,bool,string>.

Name resolution 

CLI PTB uses name resolution for common packages like sui, std, and deepbook, so you can use them directly instead of their addresses: 0x2, 0x1, or 0xdee9. You can still use the traditional package::module::function way to call a specific function. For example, you can use both std::option or 0x1::option

sui client ptb \
--assign A none \
--move-call std::option::is_none "<u64>" A \
--gas-budget 5000000

You can also refer to addresses in your local wallet by their alias instead of their full address. 

Assign data to variables

The --assign argument binds values to variables. There are a few ways you can use it: 

  1. Assign a value to a variable: 
sui client ptb \
--assign myvar 100 \
--gas-budget 5000000
  1. Assign a variable to the result of the previous transaction. In the following example, split-coins results in three new objects, which will be accessible via the coins variable due to the usage of --assign
sui client ptb \
--split-coins gas "[1000,2000,3000]" \
--assign coins \
--gas-budget 5000000
  1. Assign a variable to a different variable
sui client ptb \  
--assign myvar vector[100, 200] \  
--assign my_addr @0x0fe375fff0ee40d20c54a7f2478b9b5c7eaa3625b761 \  
--assign old_address my_addr \  
--gas-budget 5000000

Publish package and call function 

In the Sui repository, the first package example is a simple Move project. In a nutshell, this example publishes that package, which creates a Forge object that you can use to create swords with the new_sword function. The code below assumes that you have a Sui Testnet environment set up and sufficient gas for your address. 

First, access the example folder and publish the package. 

cd sui/examples/move/first_package
sui client ptb 
--move-call sui::tx_context::sender \
--assign "sender" \
--publish "." \
--assign upgrade_cap \
--transfer-objects [upgrade_cap] sender \
--gas-budget 50000000

Publishing this package results in the following transaction data:

INCLUDING DEPENDENCY Sui
INCLUDING DEPENDENCY MoveStdlib
BUILDING first_package
Successfully verified dependencies on-chain against source.
╭──────────────────────────────────────────────────────╮
│ PTB Execution Summary                                │
├──────────────────────────────────────────────────────┤
│ Digest: 8ULSyjJXhX5CPiEge3a7T2kLSA3YA8V44BAGZPdknJ1h │
│ Status: success                                      │
│ Gas Cost Summary:                                    │
│   Storage Cost: 9978800                              │
│   Computation Cost: 1000000                          │
│   Storage Rebate: 978120                             │
│   Non-refundable Storage Fee: 9880                   │
╰──────────────────────────────────────────────────────╯

From the previous result, we can use the digest and pass it to sui client tx-digest to find the packageId and the objectId for the Forge object. We want to forge a new sword and transfer it to the sender of this transaction. 

Next, we look up the function new_sword from the package we published to understand what arguments we need to pass to the function. Its signature is:

/// Constructor for creating swords 
 public fun new_sword( 
     forge: &mut Forge, 
     magic: u64, 
     strength: u64, 
     ctx: &mut TxContext, 
 ): Sword 

Now that the package exists onchain, let’s use a PTB to create a sword. The first thing to do is to get the sender’s address with the move-call command and assign the result of that call to the sender variable. We can use assign for the magic and strength arguments, and assign the Forge object ID to the variable forge_obj.

Finally, we call the new_sword function through the move-call command, pass all the right function arguments, assign the result of the move-call to the sword variable, and transfer the new sword object to the sender. 

sui client ptb \
--move-call sui::tx_context::sender \
--assign sender \
--assign magic 10u64 \
--assign strength 50u64 \
--assign forge_obj @0xd5d19fde64ea43876add02efb2a769372869f30118 \
--move-call  0xbe6edeefea02371d35fdde5a45033d8d8e2e00b3eb911b23fa::example::new_sword  forge_obj magic strength \
--assign sword \
--transfer-objects "[sword]" sender \
--gas-budget 5000000 \
--summary

Because we've set the --summary flag, the execution result reads:

╭──────────────────────────────────────────────────────╮ 
│ PTB Execution Summary                                │ 
├──────────────────────────────────────────────────────┤ 
│ Digest: HYqWpN9sYBGLawj4BE9VnRM7qaezUFA8aLrGBbjf72L7 │ 
│ Status: success                                      │ 
│ Gas Cost Summary:                                    │ 
│    Storage Cost: 3739200                             │ 
│    Computation Cost: 1000000                         │ 
│    Storage Rebate: 2309868                           │ 
│    Non-refundable Storage Fee: 23332                 │ 
╰──────────────────────────────────────────────────────╯ 

PTBs in a Bash script

To facilitate automation and reusability, you can copy and paste the PTB commands from the terminal into a Bash script and run it. 

#!/usr/bin/bash 

sui client ptb \ 
--assign to_address @0x02a212de6a9dfa3a69e22387acfbafbb1a9e59
--split-coins gas "[1000,2000,3000]" \
--assign coins \
--transfer-objects "[coins.0, coins.1, coins.2]" to_address \
--gas-budget 5000000

The above code can be run as a normal Bash script.

Mint 100 NFTs and transfer them using PTBs in a Bash script

This example uses a Bash script to mint 100 NFTs and transfer them to 100 addresses. The NFT is defined in this smart contract posted to GitHub. We publish the package, and get the num_issuer_cap and the package IDs. The project defines the function mint in the module num, which we declare.

The script then loads the addresses from a text file where each address is on a new line. It then builds a string of move-call commands, calling the mint function and transferring the object to an address, all in a for loop. Finally, the PTB is executed.

Note that, being a Bash script, you can pass in environment variables as you’d do with any other Bash script.

num_issuer_cap=0x3e8a046807edd63b75014b9aff0f8dc30b76344a4dfdb1ae4655932e0514086b
package=0x610834b5fa960b297a5caf6af56bcc045d67d158889da5b511a20031307ce099
module=num
fun=mint

echo "Loading addresses from file"
readarray -t addresses < addresses.txt

cmd=""
for address in ${addresses[@]}; do
  cmd+="--move-call $package::$module::$fun @$num_issuer_cap --assign X --transfer-objects [X] @$address "
done

echo "Executing PTB"
sui client ptb \
 $cmd \
 --gas-budget 5000000000 \
 --summary

The end result is:

bash-5.2$ bash working-demo.sh
Loading addresses from file
Executing PTB
╭──────────────────────────────────────────────────────╮
│ PTB Execution Summary                                │
├──────────────────────────────────────────────────────┤
│ Digest: 7THBdpRHWSTiWExxSB7A3m9E7PDP9kQaD16N8xXevdcA │
│ Status: success                                      │
│ Gas Cost Summary:                                    │
│   Storage Cost: 132376800                            │
│   Computation Cost: 1000000                          │
│   Storage Rebate: 2392632                            │
│   Non-refundable Storage Fee: 24168                  │
╰──────────────────────────────────────────────────────╯

Purchase of a domain on SuiNS 

The following example registers a Sui domain name by first creating a zero coin that has no SUI, to be used for move-call, and after that it transfers the newly created domain name object to a specific address. 

sui client ptb \
--assign suins_object_id  @0x300369e8909b9a6464da265b9a5a9ab6fe2158a040e84e808628cde7a07ee5a3 \
--split-coins gas "[0]" \
--assign zero_coin \
--move-call 0x4255184a0143c0ce4394a3f16a6f5aa5d64507269e54e51ea396d569fe8f1ba5::register::register suins_object_id '"mysui.sui"' 1u8 zero_coin @0x6 \
--assign suins_name \
--transfer-objects "[suins_name]"  @0x0fe375fff0ee40d20c54a7f2478b9b5c7eaa3625b7611f9661ec5faefb4a6fea \
--gas-budget 50000000

The result of this command is:

╭──────────────────────────────────────────────────────╮
│ PTB Execution Summary                                │
├──────────────────────────────────────────────────────┤
│ Digest: 5D2W4beXGWzF4Cn69y6ihnTvoyBC99vb5zGhpxAhdq1z │
│ Status: success                                      │
│ Gas Cost Summary:                                    │
│   Storage Cost: 10039600                             │
│   Computation Cost: 1000000                          │
│   Storage Rebate: 5394708                            │
│   Non-refundable Storage Fee: 54492                  │ 
╰──────────────────────────────────────────────────────╯

Preview a PTB instead of executing 

The --preview flag, useful for complex PTBs, lists the transactions in the PTB instead of executing it. The preview displays a well-formatted table with each transaction and its arguments after parsing the entire input. It also displays any errors. This helps when dealing with complex PTBs, as it will produce the list of transactions instead of executing the PTB. 

sui client ptb \
--assign to_address @0x02a212de6a9dfa3a69e22387acfbafbb1a9e59 \
--split-coins gas [1000,2000,3000] \
--assign coins \
--transfer-objects [coins.0, coins.1, coins.2] to_address \
--gas-budget 5000000 \
--summary \
--preview

And the result of this call would be the following: 

Click image to expand it.

Warnings and errors 

When using the --assign command and the --warn-shadows flag, the tool issues a warning when it encounters shadowed variables. That is, if there are multiple declarations for a given variable name, the tool warns the developer that the same variable is declared multiple times. Here is an example:

sui client ptb \ 
--assign a 10 \ 
--assign a 15 \ 
--gas-budget 5000000 \ 
--warn-shadows 

The image below shows the warnings produced when building a PTB: 

Click image to expand it.

One area where we dedicated a lot of effort is on how to report useful errors. We aimed for errors to include sufficient information about what is wrong, pointing exactly to the argument that leads to that error, and in some cases even providing a hint on how to fix it.  

For example, if the user calls the std::Option::is_none function and passes the module name with an uppercase instead of lowercase, the tool will produce the following message: 

sui client ptb \  
--move-call std::Option::is_none none \ 
--gas-budget 5000000

And the error message will show the following: 

Click image to expand it.

 Here is another example which showcases the error produced when the user calls --assign X without passing a variable, after a command that does not produce any result: 

sui client ptb \ 
--assign X \ 
--gas-budget 5000000

The error in this case would be the following:

Click image to expand it.

 Here is a another example regarding reserved words in the tool: 

sui client ptb --assign address 5 --gas-budget 5

The error message here would show:

Click image to expand it.