Companion code: https://github.com/mrgnresearch/sui-cookbook/tree/main/programmable-transactions-rust
Overview
Programmable Transaction Blocks (PTB) are a powerful concept in Sui. They allow users to atomically execute a sequence (block) of transactions. For the ones familiar with Solana, it is tempting to make the following comparison:
Sui PTB ↔ Solana transaction
Sui transaction ↔ Solana instruction
While there is some amount of equivalence to draw, a crucial difference lies is the ability for Sui PTBs to easily route inputs/results between internal transactions. This routing enables developers to craft complex, cross-contract transactions entirely client-side, where a custom contract deployment would be required on other chains. While there is also likely compromise involved in the way those PTBs are implemented compared to other chains’ equivalents, this alone is a pretty compelling feature.
While there are a few examples available to demonstrate how to use PTBs in Typescript (including the official doc), the rust examples are scarcer and less advanced. This seems to have a few developers stumble, as using them in Rust requires to understand more precisely the types manipulated.
This article provides some level of explanation on a sample crate which constructs and runs a complex PTB using the Rust SDK. The code is available here. If that is all you need you’re all set, but if you’d like some additional details you are welcome to read on.
Show me some code
Let’s take an utterly useless and complex PTB as our use case:
This masterpiece is a representation of a PTB which performs a variety of actions, such as:
reading coin objects values
performing math on the outputs of these values
creating/destroying coins
transferring coins
creating objects (a kiosk)
etc
(the actions in themselves are of little importance)
Creating a PTB boils down to a few tasks:
declaring a PTB builder
defining entry arguments, be them pure (e.g. u64s, addresses, IDs), shared objects (e.g. liquidity pools in most cases), or owned/immutable objects (e.g. coins, Clock)
creating relevant transactions in order, using the usual
package
/module
/function
triplet, and providing type arguments and call arguments for each one of them, understanding that the call arguments can either be:the ones defined by you from external data (e.g. coins, pure arguments)
all or part of the result of any prior command in the block
wrapping up the PTB
Let’s illustrate each of these through our example code.
#1 Declaring the builder
That was easy.
#2 Defining entry arguments
In this example, we are providing exactly 2 entry arguments:
a SUI
Coin
we are going to manipulatea number, which we use in a math function call transaction
They are constructed using external data obtained through RPC calls like so:
Prefer being explicit when creating numeric pure arguments (e.g.
let number_two_arg = pt_builder.pure(2u64)?;
) or you will faceVMVerificationOrDeserializationError
s.The reason for it is that Rust will default to 32-bit integers when given the choice, which will lead to this argument being serialized as such, and a subsequent
ExecutionError
when the VM attempts to deserialize this argument as theu64
the function expects, for instance.
#3 Creating individual transactions
These commands can be move calls or native function calls. For instance our sample PTB makes many move calls, but also calls TransferObjects
a couple times (a native function).
Let’s illustrate each.
Move calls
As mentioned above, the arguments are standard, with the usual function “address” triplet, the type arguments, and the call arguments.
The important thing to notice is the type of the arguments provided: Argument
. Looking at the sui crates, it shows:
Ignoring GasCoin
here, all there is to understand is that any “entry” input you provide (like original_coin_arg
and number_two_arg
in the previous section) have their contents stored in the builder1, and are then reduced to an Argument::Input
, or in other words: a simple index pointing to the relevant input.
Similarly, on the call result side, ProgrammableTransactionBuilder::programmable_move_call
returns an Argument
, and specifically an Argument::Result
. As shown above, it is no different from Input
in that it is an index i
pointing to the result of the i
th transaction in the block.
Coming back to the join
move call snippet above, you can see that the first argument passed is an entry arg, i.e. Argument::Input
, whereas the second one is a reference to the result of a prior move call, i.e. Argument::Result
. This shows how inputs/results can be weaved along calls to allow execution of complex logic across any contract, all from the client side.
The last variant in the Argument
enum, NestedResult
, can be used in the case where several variables are returned from a call (i.e. a tuple), and you need to direct one of those items as input to posterior transactions. The idea is then to:
retrieve the underlying result index
i
in the relevant call’sResult
, andconstruct the next call’s input as an
Argument::NestedResult(i, N)
,N
being the index of the item you need in the tuple.
This is illustrated in the following snippet:
Native calls
This one is pretty self-explanatory. The only point to note is the existence of a couple variants of this transfer function, to accommodate providing Argument
s or generic types (here we are using the former since we are providing a result).
#4 Wrapping up the block
A look under the hood
The sample crate makes use of dev_inspect_transaction_block
2, which allows to simulate and inspect the results of the block. It retrieves a few relevant results and performs some check, as a way to anchor this concept of results, and as a free demonstration of object deserialization in Rust.
A good exercise to really appropriate what happens during PTB construction is to print the PTB itself after it has been finish()
ed, and realize that all there is to it is:
a list of (potentially factored) inputs, e.g.:
a list of commands, some of them potentially referring to one or more of the inputs above, e.g.:
That’s all folks, good luck.
Companion code: https://github.com/mrgnresearch/sui-cookbook/tree/main/programmable-transactions-rust
and unless specified factored in case of duplicate args