diff --git a/docs/content/concepts/sui-move-concepts/conventions.mdx b/docs/content/concepts/sui-move-concepts/conventions.mdx index d97f0f7773fe1..1589f2cf22ab3 100644 --- a/docs/content/concepts/sui-move-concepts/conventions.mdx +++ b/docs/content/concepts/sui-move-concepts/conventions.mdx @@ -1,494 +1,450 @@ --- -title: Conventions -description: Recommended Move 2024 best practices for the Sui blockchain. +title: Move Conventions +description: Recommended Move 2024 best practices for Sui development. --- -The following recommendations are based on 2024 Move. +This guide outlines recommended conventions and best practices for writing Move smart contracts on Sui. Following these guidelines helps create more maintainable, secure, and composable code that aligns with ecosystem standards. -## Add section titles +While these conventions are recommendations rather than strict rules, they represent patterns that have proven effective across many Sui projects. They help create consistency across the ecosystem and make code easier to understand and maintain. -Use titles in code comments to create sections for your Move code files. Structure your titles using `===` on either side of the title. +## Organization principles -```move -module conventions::comments { - // === Imports === +### Package - // === Errors === +A Sui package consists of: +- a `sources` directory containing the Move code to be uploaded to the blockchain +- a `Move.toml` manifest file that declares dependencies and other information about the package +- a `Move.lock` file that the Sui Move toolchain automatically generates to lock the versions of the dependencies and track the different published and upgraded versions of the package that exist on the different networks - // === Constants === +For this reason, the `Move.lock` file should always be part of the package (don't add it to the `.gitignore` file). Use the [automated address management](https://docs.sui.io/concepts/sui-move-concepts/packages/automated-address-management) instead of the old `published-at` field in the manifest file. - // === Structs === +Optionally, you can add a `tests` directory to contain the tests for the package and an `examples` directory to provide use cases for the package. Neither directory is uploaded on chain when you publish the package. - // === Method Aliases === +``` +sources/ + my_module.move + another_module.move + ... +tests/ + my_module_tests.move + ... +examples/ + using_my_module.move +Move.lock +Move.toml +``` - // === Public-Mutative Functions === +In your package manifest, the package name should be in PascalCase: `name = "MyPackage"`. Ideally, the named address representing the package should be the same as the package name, but in snake_case: `my_package = 0x0`. - // === Public-View Functions === +### Modules - // === Admin Functions === +Modules are the main building blocks of your Move code. They are used to organize and encapsulate related functionality. Design your modules around one object or data structure. A variant structure should have its own module to avoid complexity and bugs. - // === Public-Package Functions === +Module declarations don't need to use brackets anymore and the compiler provides default `use` statements for widely used modules, so you don't need to declare all of them. - // === Private Functions === +```move +module conventions::wallet; - // === Test Functions === +public struct Wallet has key, store { + id: UID, + amount: u64 } -``` -## CRUD functions names -These are the available CRUD functions: +module conventions::claw_back_wallet; -- `add`: Adds a value. -- `new`: Creates an object. -- `drop`: Drops a struct. -- `empty`: Creates a struct. -- `remove`: Removes a value. -- `exists_`: Checks if a key exists. -- `contains`: Checks if a collection contains a value. -- `destroy_empty`: Destroys an object or data structure that has values with the **drop** ability. -- `to_object_name`: Transforms an Object X to Object Y. -- `from_object_name`: Transforms an Object Y to Object X. -- `property_name`: Returns an immutable reference or a copy. -- `property_name_mut`: Returns a mutable reference. +public struct Wallet has key { + id: UID, + amount: u64 +} +``` -## Potato structs +### Body -Do not use 'potato' in the name of structs. The lack of abilities define it as a potato pattern. +Structure your code using comments to create sections for your Move code files. Structure your titles using `===` on either side of the title. ```move -module conventions::request { - // ✅ Right - struct Request {} +module conventions::comments; - // ❌ Wrong - struct RequestPotato {} -} -``` +// === Imports === -## Read functions +// === Errors === -Be mindful of the dot syntax when naming functions. Avoid using the object name on function names. +// === Constants === -```move -module conventions::profile { +// === Structs === - struct Profile { - age: u64 - } +// === Events === - // ✅ Right - public fun age(self: &Profile): u64 { - self.age - } +// === Method Aliases === - // ❌ Wrong - public fun profile_age(self: &Profile): u64 { - self.age - } -} +// === Public Functions === -module conventions::defi { +// === View Functions === - use conventions::profile::{Self, Profile}; +// === Admin Functions === - public fun get_tokens(profile: &Profile) { +// === Package Functions === - // ✅ Right - let name = profile.age(); +// === Private Functions === - // ❌ Wrong - let name2 = profile.profile_age(); - } -} +// === Test Functions === ``` -## Empty function +Here, "public functions" are the functions modifying state, "view functions" are often on-chain getters or off-chain helpers. The latter are not necessary because you can query objects to read their data. The `init` function should be the first function in the module, if it exists. -Name the functions that create data structures as `empty`. +Try to sort your functions by their purpose and according to the user flow to improve readability. You can also use explicit function names like `admin_set_fees` to make it clear what the function does. -```move -module conventions::collection { +Ideally, test functions should only consist of `[test_only]` helpers for the actual tests that are located in the `tests` directory. - struct Collection has copy, drop, store { - bits: vector - } +Group imports by dependency, for example: - public fun empty(): Collection { - Collection { - bits: vector[] - } - } -} +```move +use std::string::String; +use sui::{ + coin::Coin, + balance, + table::Table +}; +use my_dep::battle::{Battle, Score}; ``` -## New function +## Naming conventions + +Adhering to naming conventions in your code helps readability and ultimately makes your codebase easier to maintain. The following sections outline the key naming conventions to follow when writing Move code. -Name the functions that create objects as `new`. +### Constants + +Constants should be uppercase and formatted as snake case. Errors are specific constants that use PascalCase and start with an E. Make them descriptive. ```move -module conventions::object { +module conventions::constants; - use sui::object::{Self, UID}; - use sui::tx_context::TxContext; +// correct non-error constant +const MAX_NAME_LENGTH: u64 = 64; - struct Object has key, store { - id: UID - } +// correct error constant +const EInvalidName: u64 = 0; - public fun new(ctx:&mut TxContext): Object { - Object { - id: object::new(ctx) - } - } -} +// wrong error constant +const E_INVALID_NAME: u64 = 0; ``` -## Shared objects +### Structs -Library modules that share objects should provide two functions: one to create the object and another to share it. It allows the caller to access its UID and run custom functionality before sharing it. +Always declare struct abilities in this order: `key`, `copy`, `drop`, `store`. -```move -module conventions::profile { +Do not use 'potato' in the name of structs. The lack of abilities define it as a potato pattern. - use sui::object::{Self, UID}; - use sui::tx_context::TxContext; - use sui::transfer::share_object; +Structs support positional fields that can be used for simple wrappers, dynamic field keys, or as tuples. - struct Profile has key { - id: UID - } +Use the `Event` suffix to name structs that emit events. - public fun new(ctx:&mut TxContext): Profile { - Profile { - id: object::new(ctx) - } - } +```move +module conventions::request; - public fun share(profile: Profile) { - share_object(profile); - } +// dynamic field keys +public struct ReceiptKey(ID) has copy, drop, store; + +// dynamic field +public struct Receipt has key, store { + id: UID, + data: Data } + +// right naming +public struct Request(); + +// wrong naming +public struct RequestPotato {} ``` -## Reference functions +### CRUD function names -Name the functions that return a reference as `_mut` or ``, replacing with `` the actual name of the property. +The following functions follow standard CRUD (Create, Read, Update, Delete) naming conventions: -```move -module conventions::profile { +- `new`: Creates an empty object. +- `empty`: Creates an empty struct. +- `create`: Creates an initialized object or struct. +- `add`: Adds a value. +- `remove`: Removes a value. +- `exists`: Checks if a key exists. +- `contains`: Checks if a collection contains a value. +- `borrow`: Returns an immutable reference of a struct or object. +- `borrow_mut`: Returns a mutable reference of a struct or object. +- `property_name`: Returns an immutable reference or a copy of a field. +- `property_name_mut`: Returns a mutable reference of a field. +- `drop`: Drops a struct. +- `destroy`: Destroys an object or data structure that has values with the **drop** ability. +- `destroy_empty`: Destroys an empty object or data structure that has values with the **drop** ability. +- `to_name`: Transforms a Type X to Type Y. +- `from_name`: Transforms a Type Y to Type X. - use std::string::String; +### Generics - use sui::object::UID; +Declare generics using single letter names or full names. By convention, developers use `T` and `U` for generic types, but you can use a more descriptive name if it is not confusing with other types. Always prioritize readability. - struct Profile has key { - id: UID, - name: String, - age: u8 - } +```move +module conventions::generics; - // profile.name() - public fun name(self: &Profile): &String { - &self.name - } +// single letter name +public struct Receipt has store { ... } - // profile.age_mut() - public fun age_mut(self: &mut Profile): &mut u8 { - &mut self.age - } -} +// full name +public struct Receipt has store { ... } ``` -## Separation of concerns +## Code Structure + +The following section covers common patterns and best practices specific to Move development on Sui, including object ownership models and function design principles. + +### Shared objects -Design your modules around one object or data structure. A variant structure should have its own module to avoid complexity and bugs. +Library modules that share objects should provide two functions: one to instantiate and return the object, and another one to share it. It allows the caller to pass it to other functions and run custom functionality before sharing it. ```move -module conventions::wallet { +module conventions::shop; - use sui::object::UID; +public struct Shop has key { + id: UID +} - struct Wallet has key, store { - id: UID, - amount: u64 +public fun new(ctx: &mut TxContext): Shop { + Shop { + id: object::new(ctx) } } -module conventions::claw_back_wallet { - - use sui::object::UID; - - struct Wallet has key { - id: UID, - amount: u64 - } +public fun share(shop: Shop) { + transfer::share_object(shop); } ``` -## Errors +### Pure functions -Use PascalCase for errors, start with an E and be descriptive. +Keep your functions pure to maintain composability. Do not use `transfer::transfer` or `transfer::public_transfer` inside core functions, except in specific cases where the object is not transferable and shouldn't be modified. ```move -module conventions::errors { - // ✅ Right - const ENameHasMaxLengthOf64Chars: u64 = 0; - - // ❌ Wrong - const INVALID_NAME: u64 = 0; -} -``` +module conventions::amm; -## Struct property comments +use sui::coin::Coin; -Describe the properties of your structs. +public struct Pool has key { + id: UID +} -```move -module conventions::profile { +// right -> returns the excess coins even if they have zero value. +public fun add_liquidity(pool: &mut Pool, coin_x: Coin, coin_y: Coin): (Coin, Coin, Coin) { + // Implementation omitted. + abort(0) +} - use std::string::String; +// right but not recommended +public fun add_liquidity_and_transfer(pool: &mut Pool, coin_x: Coin, coin_y: Coin, recipient: address) { + let (lp_coin, coin_x, coin_y) = add_liquidity(pool, coin_x, coin_y); + transfer::public_transfer(lp_coin, recipient); + transfer::public_transfer(coin_x, recipient); + transfer::public_transfer(coin_y, recipient); +} - use sui::object::UID; +// wrong +public fun impure_add_liquidity(pool: &mut Pool, coin_x: Coin, coin_y: Coin, ctx: &mut TxContext): Coin { + let (lp_coin, coin_x, coin_y) = add_liquidity(pool, coin_x, coin_y); + transfer::public_transfer(coin_x, tx_context::sender(ctx)); + transfer::public_transfer(coin_y, tx_context::sender(ctx)); - struct Profile has key, store { - id: UID, - /// The age of the user - age: u8, - /// The first name of the user - name: String - } + lp_coin } ``` -## Destroy functions +### Coin argument -Provide functions to delete objects. Destroy empty objects with the function `destroy_empty`. Use the function `drop` for objects that have types that can be dropped. +Pass the `Coin` object by value with the exact right amount directly to improve transaction readability from the frontend. ```move -module conventions::wallet { +module conventions::amm; - use sui::object::{Self, UID}; - use sui::balance::{Self, Balance}; - use sui::sui::SUI; +use sui::coin::Coin; - struct Wallet has key, store { - id: UID, - value: Value - } +public struct Pool has key { + id: UID +} - // Value has drop - public fun drop(self: Wallet) { - let Wallet { id, value: _ } = self; - object::delete(id); - } +// right +public fun swap(coin_in: Coin): Coin { + // Implementation omitted. + abort(0) +} - // Value doesn't have drop - // Throws if the `wallet.value` is not empty. - public fun destroy_empty(self: Wallet>) { - let Wallet { id, value } = self; - object::delete(id); - balance::destroy_zero(value); - } +// wrong +public fun exchange(coin_in: &mut Coin, value: u64): Coin { + // Implementation omitted. + abort(0) } ``` -## Pure functions +### Access control -Keep your functions pure to maintain composability. Do not use `transfer::transfer` or `transfer::public_transfer` inside core functions. +To maintain composability, use capability objects instead of arrays of addresses for access control. ```move -module conventions::amm { +module conventions::access_control; - use sui::transfer; - use sui::coin::Coin; - use sui::object::UID; - use sui::tx_context::{Self, TxContext}; - - struct Pool has key { - id: UID - } +use sui::sui::SUI; +use sui::balance::Balance; +use sui::coin::{Self, Coin}; +use sui::table::{Self, Table}; - // ✅ Right - // Return the excess coins even if they have zero value. - public fun add_liquidity(pool: &mut Pool, coin_x: Coin, coin_y: Coin): (Coin, Coin, Coin) { - // Implementation omitted. - abort(0) - } - - // ✅ Right - public fun add_liquidity_and_transfer(pool: &mut Pool, coin_x: Coin, coin_y: Coin, recipient: address) { - let (lp_coin, coin_x, coin_y) = add_liquidity(pool, coin_x, coin_y); - transfer::public_transfer(lp_coin, recipient); - transfer::public_transfer(coin_x, recipient); - transfer::public_transfer(coin_y, recipient); - } - - // ❌ Wrong - public fun impure_add_liquidity(pool: &mut Pool, coin_x: Coin, coin_y: Coin, ctx: &mut TxContext): Coin { - let (lp_coin, coin_x, coin_y) = add_liquidity(pool, coin_x, coin_y); - transfer::public_transfer(coin_x, tx_context::sender(ctx)); - transfer::public_transfer(coin_y, tx_context::sender(ctx)); - - lp_coin - } +public struct Account has key, store { + id: UID, + balance: u64 } -``` -## Coin argument - -Pass the `Coin` object by value with the right amount directly because it's better for transaction readability from the frontend. +public struct State has key { + id: UID, + // field not necessary as the state lives in the Account objects + accounts: Table, + balance: Balance +} -```move -module conventions::amm { +// right -> with this function, another protocol can hold the `Account` on behalf of a user. +public fun withdraw(state: &mut State, account: &mut Account, ctx: &mut TxContext): Coin { + let authorized_balance = account.balance; - use sui::coin::Coin; - use sui::object::UID; + account.balance = 0; - struct Pool has key { - id: UID - } + coin::take(&mut state.balance, authorized_balance, ctx) +} - // ✅ Right - public fun swap(coin_in: Coin): Coin { - // Implementation omitted. - abort(0) - } +// wrong -> this is less composable. +public fun wrong_withdraw(state: &mut State, ctx: &mut TxContext): Coin { + let sender = tx_context::sender(ctx); - // ❌ Wrong - public fun exchange(coin_in: &mut Coin, value: u64): Coin { - // Implementation omitted. - abort(0) - } + let authorized_balance = table::borrow_mut(&mut state.accounts, sender); + let value = *authorized_balance; + *authorized_balance = 0; + coin::take(&mut state.balance, value, ctx) } ``` -## Access control +### Data storage in owned vs shared objects -To maintain composability, use capabilities instead of addresses for access control. +If your dApp data has a one to one relationship, it's best to use owned objects. ```move -module conventions::access_control { - - use sui::sui::SUI; - use sui::object::UID; - use sui::balance::Balance; - use sui::coin::{Self, Coin}; - use sui::table::{Self, Table}; - use sui::tx_context::{Self, TxContext}; - - struct Account has key, store { - id: UID, - balance: u64 - } +module conventions::vesting_wallet; - struct State has key { - id: UID, - accounts: Table, - balance: Balance - } +use sui::sui::SUI; +use sui::coin::Coin; +use sui::table::Table; +use sui::balance::Balance; - // ✅ Right - // With this function, another protocol can hold the `Account` on behalf of a user. - public fun withdraw(state: &mut State, account: &mut Account, ctx: &mut TxContext): Coin { - let authorized_balance = account.balance; +public struct OwnedWallet has key { + id: UID, + balance: Balance +} - account.balance = 0; +public struct SharedWallet has key { + id: UID, + balance: Balance, + accounts: Table +} - coin::take(&mut state.balance, authorized_balance, ctx) - } - // ❌ Wrong - // This is less composable. - public fun wrong_withdraw(state: &mut State, ctx: &mut TxContext): Coin { - let sender = tx_context::sender(ctx); +// A vesting wallet releases a certain amount of coin over a period of time. +// If the entire balance belongs to one user and the wallet has no additional functionalities, it is best to store it in an owned object. +public fun new(deposit: Coin, ctx: &mut TxContext): OwnedWallet { + // Implementation omitted. + abort(0) +} - let authorized_balance = table::borrow_mut(&mut state.accounts, sender); - let value = *authorized_balance; - *authorized_balance = 0; - coin::take(&mut state.balance, value, ctx) - } +// If you wish to add extra functionality to a vesting wallet, it is best to share the object. +// For example, if you wish the issuer of the wallet to be able to cancel the contract in the future. +public fun new_shared(deposit: Coin, ctx: &mut TxContext) { + // Implementation omitted. + // shares the `SharedWallet`. + abort(0) } ``` -## Data storage in owned vs shared objects +### Admin capability -If your dApp data has a one to one relationship, it's best to use owned objects. +In admin-gated functions, the first parameter should be the capability. It helps the autocomplete with user types. ```move -module conventions::vesting_wallet { - - use sui::sui::SUI; - use sui::coin::Coin; - use sui::object::UID; - use sui::table::Table; - use sui::balance::Balance; - use sui::tx_context::TxContext; - - struct OwnedWallet has key { - id: UID, - balance: Balance - } +module conventions::social_network; - struct SharedWallet has key { - id: UID, - balance: Balance, - accounts: Table - } +use std::string::String; - /* - * A vesting wallet releases a certain amount of coin over a period of time. - * If the entire balance belongs to one user and the wallet has no additional functionalities, it is best to store it in an owned object. - */ - public fun new(deposit: Coin, ctx: &mut TxContext): OwnedWallet { - // Implementation omitted. - abort(0) - } +public struct Account has key { + id: UID, + name: String +} - /* - * If you wish to add extra functionality to a vesting wallet, it is best to share the object. - * For example, if you wish the issuer of the wallet to be able to cancel the contract in the future. - */ - public fun new_shared(deposit: Coin, ctx: &mut TxContext) { - // Implementation omitted. - // It shares the `SharedWallet`. - abort(0) - } +public struct Admin has key { + id: UID, +} + +// right -> cap.update(&mut account, b"jose".to_string()); +public fun update(_: &Admin, account: &mut Account, new_name: String) { + // Implementation omitted. + abort(0) +} + +// wrong -> account.update(&cap, b"jose".to_string()); +public fun set(account: &mut Account, _: &Admin, new_name: String) { + // Implementation omitted. + abort(0) } ``` -## Admin capability +## Documentation -In admin-gated functions, the first parameter should be the capability. It helps the autocomplete with user types. +There is nothing more pleasant than a well-written and well-documented codebase. While some argue that clean code is self-documenting, well-documented code is self-explanatory. -```move -module conventions::social_network { +### Comments - use std::string::String; +Document your code by explaining functions and structs in simple terms using the `///` syntax (doc comment). If you want to add technical insights for developers that might use your code, use the `//` syntax (regular comment). - use sui::object::UID; +Use field comments to describe the properties of your structs. In complex functions, you can also describe the parameters and return values. - struct Account has key { - id: UID, - name: String - } +```move +module conventions::hero; - struct Admin has key { - id: UID, - } +use std::string::String; +use sui::kiosk::{Kiosk, KioskOwnerCap}; - // ✅ Right - // cap.update(&mut account, b"jose"); - public fun update(_: &Admin, account: &mut Account, new_name: String) { - // Implementation omitted. - abort(0) - } +public struct Hero has key, store { + id: UID, + // power of the nft + power: u64 +} - // ❌ Wrong - // account.update(&cap, b"jose"); - public fun set(account: &mut Account, _: &Admin, new_name: String) { - // Implementation omitted. - abort(0) +/// Creates and returns a new Hero object +public fun new(ctx: &mut TxContext): Hero { + Hero { + id: object::new(ctx), + power: 0 } } + +// should be initialized before being shared +public fun initialize_hero(hero: &mut Hero) { + hero.power = 100; +} + +public fun start_battle( + self: &mut Kiosk, // user kiosk + cap: &KioskOwnerCap, // user kiosk owner cap + _policy: &TransferPolicy, // transfer policy for the game + hero_id: ID, // hero to use + battle_id: String // id of the battle to start +) { + // Implementation omitted. + abort(0) +} ``` + +### README + +Create a `README.md` file in the root of the package. Include a description of the package, the purpose of the package, and how to use it. \ No newline at end of file