Your First PTB
A Programmable Transaction Block (PTB) is SUI's way of composing multiple operations into a single atomic transaction. Unlike Ethereum where each transaction calls one function, a PTB can call multiple functions, pass results between them, and roll back everything if any step fails.
Building a Basic PTB
Here is the simplest useful PTB -- calling withdraw() on the tribe storage extension:
import { Transaction } from '@mysten/sui/transactions';
const tx = new Transaction();
tx.moveCall({
target: ${TRIBE_STORAGE_PACKAGE_ID}::tribe_storage::withdraw,
arguments: [
tx.object(TRIBE_STORAGE_CONFIG_ID), // ExtensionConfig (shared object)
tx.object(ssuId), // StorageUnit (shared object)
tx.object(characterObjectId), // Character (owned object)
tx.pure.u64(typeId), // item type ID
tx.pure.u32(quantity), // how many to withdraw
tx.pure.bool(isOwner), // whether caller is SSU owner
],
});The moveCall Method
tx.moveCall() adds a Move function call to the transaction. It takes three fields:
target
The fully-qualified function name: package_id::module_name::function_name.
target: ${TRIBE_STORAGE_PACKAGE_ID}::tribe_storage::withdrawThis maps directly to the Move function:
// module tribe_storage_access::tribe_storage
public fun withdraw(
extension_config: &ExtensionConfig,
storage_unit: &mut StorageUnit,
character: &Character,
type_id: u64,
quantity: u32,
to_ssu_owner: bool,
ctx: &mut TxContext,
) { ... }Note that ctx: &mut TxContext is implicit -- the runtime provides it automatically. You do not pass it as an argument.
arguments
An array of transaction arguments matching the Move function's parameters (minus TxContext):
arguments: [
tx.object(TRIBE_STORAGE_CONFIG_ID), // → &ExtensionConfig
tx.object(ssuId), // → &mut StorageUnit
tx.object(characterObjectId), // → &Character
tx.pure.u64(typeId), // → u64
tx.pure.u32(quantity), // → u32
tx.pure.bool(isOwner), // → bool
],typeArguments (optional)
For generic functions, you pass the type parameters:
tx.moveCall({ target:, typeArguments: [${PKG}::tribe_storage::share${EVE_FRONTIER_PKG}::character::Character], arguments: [/ ... /], });
This corresponds to the Move generic:
public fun share<T: key>(...) { ... }
// ^^^^^^ — typeArguments fills this inPure Values
Pure values are non-object arguments: integers, booleans, strings, and addresses. The SDK provides typed constructors:
tx.pure.u8(255) // u8
tx.pure.u16(65535) // u16
tx.pure.u32(4294967295) // u32
tx.pure.u64(1000000) // u64 (note: JS number is fine for reasonable values)
tx.pure.u128(...) // u128 (use BigInt for large values)
tx.pure.u256(...) // u256
tx.pure.bool(true) // bool
tx.pure.address('0xabc...') // address
tx.pure.string('hello') // vector<u8> (UTF-8 encoded string)Large Numbers
For u64 and larger types, JavaScript numbers lose precision above 2^53. Use BigInt for large values:
tx.pure.u64(9007199254740993n) // BigInt literal with 'n' suffixObject References
Object references point to on-chain objects. The simplest form is tx.object(id):
tx.object('0xdef456...') // reference to an on-chain objectThe SUI runtime determines whether the function needs &T (immutable), &mut T (mutable), or T (by value) from the Move function signature. You do not need to specify this in TypeScript.
Receiving References
For objects stored inside other objects (like OwnerCap inside a Character), you need a ReceivingRef:
import { Inputs } from '@mysten/sui/transactions';
tx.object(Inputs.ReceivingRef({
objectId: ownerCapId,
version: '12345', // current version of the object
digest: 'abc123...', // current digest of the object
}))The version and digest must match the object's current state on-chain. If they are stale, the transaction will fail. This is why you need fetchObjectRef() before building the transaction.
Sign and Execute
After building the transaction, you sign and submit it. In a dApp with wallet integration:
// Using DAppKit's signAndExecute hook
const result = await signAndExecute({ transaction: tx });For server-side scripts with a keypair:
import { Ed25519Keypair } from '@mysten/sui/keypairs/ed25519';
const keypair = Ed25519Keypair.fromSecretKey(secretKey);
const result = await client.signAndExecuteTransaction({
transaction: tx,
signer: keypair,
});Reading Transaction Results
The result object contains the transaction digest and effects:
function extractDigest(result: unknown): { digest: string; failed: boolean } | null {
if (!result || typeof result !== 'object') return null;
const r = result as Record<string, unknown>;
if (typeof r.digest === 'string') {
const effects = r.effects as Record<string, unknown> | undefined;
const status = effects?.status as Record<string, unknown> | undefined;
return { digest: r.digest, failed: status?.status === 'failure' };
}
return null;
}The digest is a unique identifier for the transaction. You can look it up on a SUI explorer:
``
https://suiexplorer.com/txblock/{digest}?network=testnet
`
Complete Example: Set Tribe Config
Here is a complete end-to-end example calling
set_tribe_config:
typescriptimport { Transaction } from '@mysten/sui/transactions';
const PACKAGE_ID = '0x789...';
const CONFIG_ID = '0xdef456...';
const ADMIN_CAP_ID = '0xabc123...';
async function setTribeConfig(
signAndExecute: (params: { transaction: unknown }) => Promise<unknown>,
tribeId: number,
): Promise<string | null> {
const tx = new Transaction();
tx.moveCall({
target: ${PACKAGE_ID}::tribe_storage::set_tribe_config,
arguments: [
tx.object(CONFIG_ID), // &mut ExtensionConfig
tx.object(ADMIN_CAP_ID), // &AdminCap
tx.pure.u32(tribeId), // tribe_id: u32
],
});
const result = await signAndExecute({ transaction: tx });
const parsed = extractDigest(result);
if (!parsed || parsed.failed) return null;
return parsed.digest;
}
Key Takeaways
- PTBs are built with
new Transaction() and composed with tx.moveCall().
target is a fully-qualified function reference: package::module::function.
arguments maps 1:1 to Move function parameters (excluding TxContext).
Use tx.pure.* for primitive values and tx.object() for on-chain objects.
ReceivingRef is required for objects nested inside other objects (like OwnerCap).
typeArguments fills in generic type parameters (e.g., ).
The runtime determines reference mutability from the Move function signature -- you do not specify & vs &mut` in TypeScript.Sign in to track your progress.