Cycle

The Scriptorium

Smart Assembly code templates and tools for on-chain development in Eve Frontier.

TypeScript PTBs for EVE Frontier
AdvancedChapter 2 of 420 min read

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:

typescript
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.

typescript
target: ${TRIBE_STORAGE_PACKAGE_ID}::tribe_storage::withdraw

This maps directly to the Move function:

move
// 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):

typescript
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:

typescript
tx.moveCall({
  target: ${PKG}::tribe_storage::share,
  typeArguments: [${EVE_FRONTIER_PKG}::character::Character],
  arguments: [/ ... /],
});

This corresponds to the Move generic:

move
public fun share<T: key>(...) { ... }
//               ^^^^^^ — typeArguments fills this in

Pure Values

Pure values are non-object arguments: integers, booleans, strings, and addresses. The SDK provides typed constructors:

typescript
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:

typescript
tx.pure.u64(9007199254740993n)  // BigInt literal with 'n' suffix

Object References

Object references point to on-chain objects. The simplest form is tx.object(id):

typescript
tx.object('0xdef456...')  // reference to an on-chain object

The 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:

typescript
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:

typescript
// Using DAppKit's signAndExecute hook
const result = await signAndExecute({ transaction: tx });

For server-side scripts with a keypair:

typescript
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:

typescript
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:

typescript
import { 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.