Building Transactions
This guide explores creating a programmable transaction block (PTB) on Sui using the TypeScript SDK and the Sui CLI.
This example constructs a PTB to send SUI tokens. First, import the Transaction class, and construct it:
import { Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
Using this, you can then add transactions to the PTB:
// Send 100 MIST to the recipient's address balance. tx.balance() draws from the
// sender's coin objects and address balance, so you do not select coins yourself.
tx.moveCall({
target: '0x2::balance::send_funds',
typeArguments: ['0x2::sui::SUI'],
arguments: [tx.balance({ balance: 100n }), tx.pure.address('0xSomeSuiAddress')],
});
A balance transfer creates no new objects for the recipient to manage. When the recipient needs a Coin<T> object instead, such as a payment a downstream Move call consumes, use tx.coin() with transferObjects:
tx.transferObjects([tx.coin({ balance: 100n })], '0xSomeSuiAddress');
You can attach multiple transaction commands of the same type to a PTB. For example, to get a list of transfers, and iterate over them to transfer coins to each:
interface Transfer {
to: string;
amount: number;
}
// Procure a list of some Sui transfers to make:
const transfers: Transfer[] = getTransfers();
const tx = new Transaction();
// tx.coin() sources each amount for you, so there is no split step to manage.
transfers.forEach((transfer) => {
tx.transferObjects([tx.coin({ balance: BigInt(transfer.amount) })], transfer.to);
});
After you define the transaction, directly execute it with a Sui client and KeyPair using client.signAndExecuteTransaction.
client.signAndExecuteTransaction({ signer: keypair, transaction: tx });
Build PTBs with the CLI
Use sui client ptb to build and execute PTBs from the command line. Prefer sui client ptb for examples that build transactions, including basic transfers and coin operations, because it exposes the same PTB commands used by SDKs.
The following CLI PTB splits 500,000,000 MIST from the gas coin and transfers the new coin to eloquent-amber:
$ sui client ptb \
--split-coins gas "[500000000]" \
--assign coin \
--transfer-objects "[coin]" eloquent-amber \
--gas-budget 5000000
Use --preview to inspect the PTB commands without executing them, --dry-run to simulate execution, and --summary for concise output after execution. For more examples, see Sui Client PTB CLI.
Constructing inputs
Inputs are how you provide external values to PTBs, for example, an amount of SUI to transfer, an object to pass into a Move call, or a shared object.
Define inputs in two ways:
-
For objects: Use the
tx.object(objectId)function to construct an input that contains an object reference. -
For pure values: Use the
tx.pure(type, value)function to construct an input for a non-object input.-
If value is a
Uint8Array, the function treats it as raw bytes and uses it directly:tx.pure(SomeUint8Array). -
Otherwise, the function generates the BCS serialization layout for the value.
-
The new version provides a more intuitive syntax, for example:
tx.pure.u64(100),tx.pure.string('SomeString'),tx.pure.address('0xSomeSuiAddress'),tx.pure.vector('bool', [true, false]).
-
Learn more about inputs.
Passing transaction results as arguments
You can use the result of a transaction command as an argument in subsequent transaction commands. Each transaction command method on the transaction builder returns a reference to the transaction result.
// Split a coin object off of the gas object:
const [coin] = tx.splitCoins(tx.gas, [tx.pure.u64(100)]);
// Transfer the resulting coin object:
tx.transferObjects([coin], tx.pure.address(address));
When a transaction command returns multiple results, you can access the result at a specific index either using destructuring or array indexes.
// Destructuring (preferred, as it gives you logical local names):
const [nft1, nft2] = tx.moveCall({ target: '0x2::nft::mint_many' });
tx.transferObjects([nft1, nft2], tx.pure.address(address));
// Array indexes:
const mintMany = tx.moveCall({ target: '0x2::nft::mint_many' });
tx.transferObjects([mintMany[0], mintMany[1]], tx.pure.address(address));
Use the gas coin
For most transactions, reach for tx.coin() and tx.balance() rather than the gas coin. They are the recommended way to source tokens, and they draw from coin objects and address balances without you selecting either.
tx.gas still matters when you want the gas coin specifically, and it is valid as input for any argument.
tx.gas is available whichever way the transaction pays for gas. When you supply coin objects, the protocol smashes them into a single gas coin. When you pass an empty array to setGasPayment([]) to draw gas from an address balance, the protocol materializes a synthetic gas coin from the sender's or sponsor's balance. tx.gas refers to that coin in either case, so building against it does not force client-side coin or balance resolution. That matters for offline building, where an unresolved tx.coin() or tx.balance() intent would need a balance lookup.
You can borrow the gas coin by reference freely: add to it with mergeCoins, split from it with splitCoins, or pass it to a Move function with moveCall. Consuming it by value is restricted to 2 commands:
transferObjects, to send the whole remaining balance to another address.sui::coin::send_funds, to deposit it into an address balance.
Any other by-value use fails with InvalidGasCoinUsage.
You can also split from and transfer other coins in your wallet using their object ID. For example,
const otherCoin = tx.object('0xCoinObjectId');
// splitCoins returns an array of coins, so destructure the first result.
const [coin] = tx.splitCoins(otherCoin, [tx.pure.u64(100)]);
tx.transferObjects([coin], address);
Get PTB bytes
If you need the PTB bytes, instead of signing or executing the PTB, you can use the build method on the transaction builder itself.
You might need to explicitly call setSender() on the PTB to ensure that the sender field is populated. This is normally done by the signer before signing the transaction, but will not be done automatically if you're building the PTB bytes yourself.
const tx = new Transaction();
// ... add some transactions...
await tx.build({ client });
In most cases, building requires a client to resolve input values such as object versions and coin selection. If you have PTB bytes, you can also convert them back into a Transaction class:
const bytes = getTransactionBytesFromSomewhere();
const tx = Transaction.from(bytes);
Building offline
To build a PTB offline, with no client available, define all input values and gas configuration yourself. For pure values, provide a Uint8Array, which the transaction uses directly. For objects, construct the reference explicitly.
// For pure values:
tx.pure(pureValueAsBytes);
// For owned or immutable objects, supply the exact version and digest:
tx.objectRef({ objectId, version, digest });
// For shared and party objects, initialSharedVersion is stable:
tx.sharedObjectRef({ objectId, initialSharedVersion, mutable });
Every offline build must also set the sender, gas price, gas budget, and gas payment. When the transaction uses no owned objects for gas or inputs, such as when setGasPayment([]) draws gas from an address balance, set a ValidDuring expiration as well.
You can then omit the client argument when calling build on the transaction. If any required data is missing, build throws an error.
Gas configuration
The transaction builder comes with default behavior for all gas logic, including automatically setting the gas price, budget, and selecting coins to be used as gas. This behavior can be customized.
Gas price
By default, the SDK sets the gas price to the reference gas price of the network. You can also explicitly set the gas price of the PTB by calling setGasPrice on the transaction builder:
tx.setGasPrice(gasPrice);
Budget
By default, the SDK automatically derives the gas budget by executing a dry-run of the PTB beforehand. The SDK then uses the dry run gas consumption to determine a balance for the transaction. You can override this behavior by explicitly setting a gas budget for the transaction using the setGasBudget on the transaction builder.
Set the gas budget in MIST, not SUI. 1 SUI is 1,000,000,000 MIST, so tx.setGasBudget(50_000_000) caps the transaction at 0.05 SUI. Take the gas price of the PTB into account when choosing a value.
tx.setGasBudget(gasBudgetAmount);
Gas payment
By default, the SDK automatically determines the gas payment. It selects all coins at the provided address that are not used as inputs in the PTB.
The SDK merges the list of coins used as gas payment down into a single gas coin before executing the PTB, and deletes all but 1 of the gas objects. The gas coin at the 0-index is the coin that all others are merged into.
// NOTE: You need to ensure that the coins do not overlap with any
// of the input objects for the PTB.
tx.setGasPayment([coin1, coin2]);
Gas coins should be objects containing the coin's { objectId: string, version: string | number, digest: string }.
App and wallet integration
The Wallet Standard interface now supports the Transaction kind directly. All signTransaction and signAndExecuteTransaction calls from apps into wallets must provide a Transaction class. You can then serialize this PTB class and send it to your wallet for execution.
To serialize a PTB for sending to a wallet, Sui recommends using the tx.serialize() function, which returns an opaque string representation of the PTB that can be passed from the Wallet Standard app context to your wallet. You can then convert this back into a Transaction using Transaction.from().
You should not build the PTB from bytes in the app code. Using serialize instead of build allows you to build the PTB bytes within the wallet itself. This allows the wallet to perform gas logic and coin selection as needed.
// Within an app
const tx = new Transaction();
wallet.signTransaction({ transaction: tx });
// Your Wallet Standard code:
function handleSignTransaction(input) {
sendToWalletContext({ transaction: input.transaction.serialize() });
}
// Within your wallet context:
function handleSignRequest(input) {
const userTx = Transaction.from(input.transaction);
}
Sponsored PTBs
The PTB builder can support sponsored PTBs by using the onlyTransactionKind flag when building the PTB.
const tx = new Transaction();
// ... add some transactions...
const kindBytes = await tx.build({ client, onlyTransactionKind: true });
// Construct a sponsored transaction from the kind bytes:
const sponsoredTx = Transaction.fromKind(kindBytes);
// You can now set the sponsored transaction data that is required:
sponsoredTx.setSender(sender);
sponsoredTx.setGasOwner(sponsor);
sponsoredTx.setGasPayment(sponsorCoins);
Learn more about sponsored transactions.
Rust SDK example
You can also build PTBs using the Sui Rust SDK. The following example demonstrates creating a ProgrammableTransactionBuilder, adding pure value and shared object inputs, and calling a Move function with type arguments:
use move_core_types::{identifier::Identifier, language_storage::TypeTag};
use sui_types::{
base_types::ObjectID,
programmable_transaction_builder::ProgrammableTransactionBuilder,
transaction::{Argument, CallArg, ObjectArg, SharedObjectMutability},
};
let mut ptb = ProgrammableTransactionBuilder::new();
// Pass a pure value
let amount = ptb.pure(1000u64)?;
// Pass a shared object
let shared_obj = ptb.obj(ObjectArg::SharedObject {
id: ObjectID::from_hex_literal("0x...")?,
initial_shared_version: 1.into(),
mutability: SharedObjectMutability::Mutable,
})?;
// Call a generic function with type arguments
ptb.programmable_move_call(
ObjectID::from_hex_literal("0xPACKAGE")?,
Identifier::new("module")?,
Identifier::new("function")?,
vec![TypeTag::U64], // type arguments (generics)
vec![shared_obj, amount], // runtime arguments
);
Passing type arguments
Generic Move functions require type parameters. Specify them as a Vec<TypeTag> in Rust or as string type tags in TypeScript:
tx.moveCall({
target: '0xPACKAGE::module::generic_function',
typeArguments: ['0x2::coin::Coin<0x2::sui::SUI>'],
arguments: [tx.object('0x...')],
});
Troubleshooting PTB errors
| Error | Cause | Fix |
|---|---|---|
| HTTP 504 (Gateway Timeout) | RPC node overloaded or unresponsive. | Retry with a different RPC endpoint or after a delay. |
MoveAbort in effects | Onchain logic failed (assertion, type mismatch). | Check the abort code against the Move module source. |
InsufficientGas | Gas budget too low for the computation. | Increase setGasBudget() or use dry-run to estimate. |
waitForTransaction
Reads are served from indexed state, which trails execution. After you submit a transaction, use waitForTransaction before you query its effects. This prevents race conditions where querying immediately after submission returns no results.
Pass the execution result directly. signAndExecuteTransaction returns a discriminated union of Transaction and FailedTransaction, so the digest is not on the result itself.
const result = await client.signAndExecuteTransaction({ transaction: tx, signer: keypair });
await client.waitForTransaction({ result });
const transaction = result.Transaction ?? result.FailedTransaction;
const txn = await client.getTransaction({
digest: transaction.digest,
include: { effects: true },
});
For end-to-end recipes covering sponsored transactions, kiosk operations, coin selection, and multi-recipient transfers, see the PTB Cookbook.