Допис
Діліться своїми знаннями.
How do I design multi-step workflows in Move?
If a process needs multiple user interactions (like a game or auction), what’s the best way to structure state and transactions?
- Move CLI
- Move Module
- Move Script
Відповіді
6You should model multi-step workflows as explicit, small state objects and safe transitions so each user action is one clear, atomic transaction: represent the workflow as a compact state machine object (or a sequence of small objects) that holds only the minimal fields needed to validate the next step, gate transitions with capabilities or ownership checks so only the right actor can advance the state, validate preconditions early and fail-fast to avoid wasting gas, keep large data off-chain and store only references or hashes on-chain, prefer owned per-user objects or dynamic fields over a single shared hotspot to avoid contention, use events for off-chain indexing and UX updates rather than on-chain logs, provide timeouts and reclaim paths for abandoned workflows, and design idempotent steps (include nonces or version numbers) so retries are safe; for example you can mint a BidRequest { id, bidder, amount, nonce } object for each bid and have a single Auction state object that your place_bid entry checks/consumes, or use a typed-state pattern Auction -> Auction -> Auction so invalid transitions are caught by types, and in the frontend use a dry-run (dryRunTransactionBlock) then submit a single PTB combining required moves (e.g., prepare bid object + call place_bid), for a minimal PTB sketch in TypeScript you can build the block like tx.moveCall({ target: ${pkg}::auction::place_bid, arguments: [tx.object(auctionId), tx.object(bidRequestId), tx.pure.u64(amount)] }) and then simulate and sign; read more about Move state patterns and PTBs in the Move book and Sui docs (https://move-book.com and https://docs.sui.io) — this approach makes each step explicit, auditable, retry-safe, and easy to index off-chain.
You should model multi-step workflows as explicit, small state objects and safe transitions so each user action is one clear, atomic transaction: represent the workflow as a compact state machine object (or a sequence of small objects) that holds only the minimal fields needed to validate the next step, gate transitions with capabilities or ownership checks so only the right actor can advance the state, validate preconditions early and fail-fast to avoid wasting gas, keep large data off-chain and store only references or hashes on-chain, prefer owned per-user objects or dynamic fields over a single shared hotspot to avoid contention, use events for off-chain indexing and UX updates rather than on-chain logs, provide timeouts and reclaim paths for abandoned workflows, and design idempotent steps (include nonces or version numbers) so retries are safe; for example you can mint a BidRequest { id, bidder, amount, nonce } object for each bid and have a single Auction state object that your place_bid entry checks/consumes, or use a typed-state pattern Auction, arguments: [tx.object(auctionId), tx.object(bidRequestId), tx.pure.u64(amount)] })
and then simulate and sign; read more about Move state patterns and PTBs in the Move book and Sui docs (https://move-book.com and https://docs.sui.io) — this approach makes each step explicit, auditable, retry-safe, and easy to index off-chain.
You should model multi-step workflows as explicit, small state objects and safe transitions so each user action is one clear, atomic transaction: represent the workflow as a compact state machine object (or a sequence of small objects) that holds only the minimal fields needed to validate the next step, gate transitions with capabilities or ownership checks so only the right actor can advance the state, validate preconditions early and fail-fast to avoid wasting gas, keep large data off-chain and store only references or hashes on-chain, prefer owned per-user objects or dynamic fields over a single shared hotspot to avoid contention, use events for off-chain indexing and UX updates rather than on-chain logs, provide timeouts and reclaim paths for abandoned workflows, and design idempotent steps (include nonces or version numbers) so retries are safe; for example you can mint a BidRequest { id, bidder, amount, nonce } object for each bid and have a single Auction state object that your place_bid entry checks/consumes, or use a typed-state pattern Auction -> Auction -> Auction so invalid transitions are caught by types, and in the frontend use a dry-run (dryRunTransactionBlock) then submit a single PTB combining required moves (e.g., prepare bid object + call place_bid), for a minimal PTB sketch in TypeScript you can build the block like tx.moveCall({ target: ${pkg}::auction::place_bid, arguments: [tx.object(auctionId), tx.object(bidRequestId), tx.pure.u64(amount)] }) and then simulate and sign; read more about Move state patterns and PTBs in the Move book and Sui docs (https://move-book.com and https://docs.sui.io) — this approach makes each step explicit, auditable, retry-safe, and easy to index off-chain.
Core Principles for Multi-Step Workflows in Move
- Represent Workflow as an Object with State
Use a struct with key ability to persist across transactions.
Add a status field (enum-like using u8 or separate structs) to track the stage.
Example:
struct Auction has key { id: UID, item_id: u64, highest_bid: u64, highest_bidder: address, status: u8 // 0 = Created, 1 = Active, 2 = Ended }
- Split Logic into Multiple Entry Functions
Each function handles one step.
Use precondition checks to enforce the correct stage.
Example:
public entry fun start_auction(auction: &mut Auction) { assert!(auction.status == 0, 1); auction.status = 1; }
public entry fun place_bid(auction: &mut Auction, bid: u64, bidder: address) { assert!(auction.status == 1, 2); assert!(bid > auction.highest_bid, 3); auction.highest_bid = bid; auction.highest_bidder = bidder; }
public entry fun close_auction(auction: &mut Auction) { assert!(auction.status == 1, 4); auction.status = 2; }
- Use Ownership to Control Who Acts
Pass mutable references (&mut) or transfer ownership to ensure the right actor controls the object.
For auctions, you might:
Keep object shared (anyone can bid).
Or move object to an escrow object controlled by the module.
- Enforce Access Control
Use TxContext::sender to check the caller.
Store owner: address or use capabilities (a resource that only the owner holds).
- Handle Partial Progress Safely
If step 2 fails, step 1 should remain valid.
Avoid global side effects until the final step (like transferring funds).
Use escrow objects for assets during workflow.
- Avoid Global Locks
Each workflow instance should be its own object.
Never put all active workflows in a single vector/map → this kills parallelism.
✅ Example: Multi-Step Auction Workflow in Sui Move
module auction::multi_step { use sui::object::{Self, UID}; use sui::tx_context::{Self, TxContext}; use sui::transfer;
struct Auction has key {
id: UID,
owner: address,
item_id: u64,
highest_bid: u64,
highest_bidder: address,
status: u8 // 0 = Created, 1 = Active, 2 = Ended
}
// Step 1: Create auction
public entry fun create(item_id: u64, ctx: &mut TxContext) {
let auction = Auction {
id: object::new(ctx),
owner: tx_context::sender(ctx),
item_id,
highest_bid: 0,
highest_bidder: tx_context::sender(ctx),
status: 0
};
transfer::transfer(auction, tx_context::sender(ctx));
}
// Step 2: Start auction
public entry fun start(auction: &mut Auction) {
assert!(auction.status == 0, 1);
assert!(auction.owner == tx_context::sender(), 2);
auction.status = 1;
}
// Step 3: Place bid
public entry fun bid(auction: &mut Auction, bid_amount: u64) {
assert!(auction.status == 1, 3);
assert!(bid_amount > auction.highest_bid, 4);
auction.highest_bid = bid_amount;
auction.highest_bidder = tx_context::sender();
}
// Step 4: Close auction
public entry fun close(auction: &mut Auction) {
assert!(auction.status == 1, 5);
assert!(auction.owner == tx_context::sender(), 6);
auction.status = 2;
// In real design: transfer item and funds here
}
}
✅ Why This Design Works
Each transaction handles one step only.
The auction object persists across steps with key.
Status enforces workflow order.
Each auction is an independent object → parallel execution possible.
Access control ensures only the right user can perform certain steps.
Alternative Approaches
Capabilities instead of status flags (e.g., ActiveAuction struct vs EndedAuction struct).
Dynamic fields to attach bids without storing all in the auction object.
Do you want me to show a capability-based design (each step creates a new object type for stronger type safety), or a dynamic field approach for storing unlimited bids without large vectors?
If you want to build a workflow in Move that needs several steps or multiple users to interact (like a game round, auction, or multi-signature process), the best approach is to represent the state of the workflow as an on-chain object. Each transaction then consumes and updates that object, ensuring the sequence follows the right order. You normally use a struct with a UID
plus fields like phase
, participants
, or bids
, so the program logic enforces which actions are valid at each stage. This way, you don’t rely on off-chain coordination — the object itself carries the workflow state and guarantees correctness through Move’s type system.
Here’s a simple auction-style example:
module auction::multi_step {
use sui::object::{Self, UID};
use sui::tx_context::TxContext;
use sui::transfer;
use std::option::{Self, Option};
/// Auction states
const STATE_OPEN: u8 = 0;
const STATE_CLOSED: u8 = 1;
struct Auction has key {
id: UID,
item: u64,
highest_bid: u64,
winner: Option<address>,
state: u8,
}
/// Create auction in "open" state
public entry fun create(item: u64, ctx: &mut TxContext) {
let auction = Auction {
id: object::new(ctx),
item,
highest_bid: 0,
winner: option::none(),
state: STATE_OPEN,
};
transfer::transfer(auction, tx_context::sender(ctx));
}
/// Step 1: Place bid (only valid if state = OPEN)
public entry fun bid(auction: &mut Auction, bid: u64, sender: address) {
assert!(auction.state == STATE_OPEN, 100);
if (bid > auction.highest_bid) {
auction.highest_bid = bid;
auction.winner = option::some(sender);
}
}
/// Step 2: Close auction (moves to CLOSED state)
public entry fun close(auction: &mut Auction) {
assert!(auction.state == STATE_OPEN, 101);
auction.state = STATE_CLOSED;
}
}
In this workflow, you create the auction first, users call bid
in step two, and finally someone calls close
to lock the state. The key idea is that the workflow object enforces order and access control, so invalid actions fail automatically. Compared to off-chain sequencing, this is safer because the blockchain itself tracks progress.
Ви знаєте відповідь?
Будь ласка, увійдіть та поділіться нею.
Move is an executable bytecode language used to implement custom transactions and smart contracts.
Зароби свою частку з 1000 Sui
Заробляй бали репутації та отримуй винагороди за допомогу в розвитку спільноти Sui.

- ... SUIMatthardy+2095
- ... SUIacher+1666
- ... SUIChubbycheeks +1091
- ... SUIjakodelarin+1060
- ... SUITucker+1047
- ... SUIKurosakisui+1034
- ... SUIOpiiii+861