Set up the Move development environment
Install the Move CLI to access the compiler, package manager, and testing framework. This toolchain is required to build and verify Move-based programming projects.
Install the Move CLI
Install the latest version using the official installer script:
curl -L https://move-language.github.io/move/install-move-cli.sh | bash
Verify the installation:
move --version
Initialize a new project
Create a new Move package. This command generates the standard directory structure, including the sources folder and Move.toml configuration file.
move init
The Move.toml file defines package metadata, dependencies, and network settings. Customize the version field to match your project's release cycle.
Verify the project structure
Ensure the environment is ready by checking the generated files. The sources directory should contain a move.mod file and a default module file.
ls -R
Expected output:
.
├── Move.toml
└── sources
└── <your_package_name>.move
Write a contract using resource types
Move treats assets as first-class citizens through resource types. Unlike standard structs, resources cannot be copied, dropped, or implicitly moved. This enforcement prevents vulnerabilities like double-spending.
Define a struct with the resource annotation. The compiler mandates explicit management of every instance. You must define functions to create, transfer, and destroy these resources.
The key capability in the struct definition is essential. It allows the resource to be stored in accounts and transferred via the standard library functions. Without key, the resource cannot be held by an account, limiting its utility as a transferable asset.
Implement programmable transaction blocks
Programmable Transaction Blocks (PTBs) allow you to bundle multiple operations into a single atomic unit. Instead of submitting separate transactions for each action, you compose a sequence of instructions that execute together. This approach reduces network overhead and ensures that dependent operations either all succeed or all fail, maintaining state consistency.
The Sui execution engine processes these instructions in order, passing resources from one instruction to the next without finalizing the state until the entire block completes. This composability is essential for complex workflows like atomic swaps, batch transfers, or multi-step asset minting.
1. Initialize the transaction builder
Start by creating a new transaction builder instance. This object acts as the container for your sequence of operations. You will chain methods onto this builder to define the logic that runs inside the block.
let builder = Transaction::begin();
2. Add resource transfers
Use the builder to move resources between accounts. Each transfer instruction takes the source object, the recipient address, and the amount. These instructions are queued but not executed until the block is signed and submitted.
let coin = builder.object(coin_object_id);
builder.transfer_objects(vec![coin], recipient_address);
3. Execute program logic
You can call smart contract functions within the PTB. These calls can read state, modify objects, or emit events. The results of earlier instructions are available to later ones, enabling complex conditional logic within the same atomic block.
let result = builder.execute_move_function(
module_id,
function_name,
vec![coin],
vec![],
);
4. Sign and submit the block
Once all instructions are chained, sign the transaction with the relevant private keys. The signed PTB is then broadcast to the network. The validator processes the entire block as a single unit, ensuring atomicity.
let tx = builder.finish();
let signed_tx = tx.sign(&private_key);
sui_client.execute_transaction_block(signed_tx).await?;
Test logic with the Move framework
Before deploying any smart contract, you must verify that your ownership rules hold up under scrutiny. Move’s type system prevents resource duplication by default, but your business logic needs explicit testing to ensure assets move correctly between accounts and are destroyed when intended.
We will walk through writing unit tests that simulate transaction execution. These tests allow you to catch logic errors early, ensuring that resources cannot be duplicated or lost, which is critical for maintaining the security guarantees Move provides.
Testing your Move code ensures that your smart contracts behave as expected before they go live. By following these steps, you can build confidence in your ownership logic and prevent costly errors on the blockchain.
Deploy to a testnet network
Before pushing your Move module to the mainnet, you must verify its behavior on a testnet. This step isolates your logic from real financial risk while confirming that resource ownership and access controls function as intended. We will use the Sui testnet as our primary example, though the workflow mirrors Aptos closely.
Pre-deployment checklist
-
Verify gas limits cover bytecode size.
-
Confirm testnet faucet tokens are in wallet.
-
Check address format matches network standard.
-
Ensure no local-only imports remain in code.
Common Move programming mistakes
Move’s ownership model prevents data races, but it requires strict discipline. Developers coming from Rust often struggle with the language’s resource-oriented nature, where values cannot be implicitly copied or dropped. Misunderstanding these rules leads to compilation errors that halt development before deployment.
Violating Ownership Rules
The most frequent error is attempting to use a resource after it has been moved. In Move, resources are unique and linear; they must be explicitly consumed or destroyed. If you try to access a variable after moving it to a struct or passing it to another function, the compiler will reject the code.
public fun transfer_token(token: Token, recipient: address) {
// Error: use of moved value 'token'
log_token(token);
destroy(token);
}
Always ensure you are not referencing a value after it has been transferred. Treat resources like physical keys: once you hand them over, you no longer have access to them.
Misusing Dynamic Fields
Dynamic fields allow structs to hold other structs, but they require careful handling. A common pitfall is forgetting to destroy the dynamic field when the parent struct is destroyed. If you don’t explicitly destroy the nested resource, it leaks, causing memory issues on-chain.
To avoid these mistakes, always trace the lifecycle of your resources. Ensure every move operation is accounted for and that dynamic fields are properly cleaned up when their parent is no longer needed.


No comments yet. Be the first to share your thoughts!