Skip to main content

Getting Started with Move Coffee Token Tutorial

Introduction

Welcome to this beginner-friendly tutorial on Move programming! You will build a simple Coffee Shop example that allows customers to buy, claim, and transfer coffee tokens. The example demonstrates fundamental concepts like importing modules, defining constants, creating structs, minting and burning tokens, and handling control flow.

Key Concepts Covered

  1. Importing modules: You will import essential Move packages for transactions, tokens, and balances.
  2. Defining constants: Used to represent error codes and set a price for coffee.
  3. Creating structs with abilities: Defined the COFFEE and CoffeeShop structs to represent tokens and shop data.
  4. Initialization with OTW: Set up the shop with its treasury cap for minting tokens.
  5. Control flow with assertions: Ensured correct amounts and balances in transactions.
  6. Transferring and burning tokens: Demonstrated how tokens are moved between users and how they can be consumed (burned).

Prerequisites

1. Create an IOTA Move Package

Use the following command to create a standard IOTA Move package called coffee_shop:

iota move new coffee_shop

The command will create and populate the coffee_shop directory with a skeleton for an IOTA Move project. To add a module, create a .move file in the sources directory, and call it coffee.move. In coffee.move, paste the following codes to the coffee module:

2. Importing Modules from the IOTA Package

The first step in our program is importing necessary modules from the IOTA package. These modules provide functionalities for transaction handling, coins, balances, and tokens.

    use iota::coin::{Self, TreasuryCap, Coin};
use iota::balance::{Self, Balance};
use iota::token::{Self, Token};
use iota::iota::IOTA;

3. Defining Constants

In this step, you should define a few constants that represent different error codes and the price of a coffee. Constants in Move are useful for defining fixed values used throughout the code.

    const EIncorrectAmount: u64 = 0;
/// Trying to claim a free coffee without enough points.
/// Or trying to transfer but not enough points to pay the commission.
const ENotEnoughPoints: u64 = 1;

/// 10 IOTA for a coffee.
const COFFEE_PRICE: u64 = 10_000_000_000;

  • EIncorrectAmount: Error when the wrong amount is provided.
  • ENotEnoughPoints: Error when there aren’t enough points to complete a transaction.
  • COFFEE_PRICE: The price of a coffee (10 IOTA).

4. Creating Structs with Abilities

Structs in Move represent data storage objects, and you will define two structs in this program.

Abilities in Move are a typing feature in Move that control what actions are permissible for values of a given type.

  1. COFFEE: A struct representing coffee points.
  2. CoffeeShop: A struct for the coffee shop that includes its treasury cap and IOTA balance.
    public struct COFFEE has drop {}

/// The shop that sells Coffee and allows to buy a Coffee if the customer
/// has 10 COFFEE points.
public struct CoffeeShop has key {
id: UID,
/// The treasury cap for the `COFFEE` points.
coffee_points: TreasuryCap<COFFEE>,
/// The IOTA balance of the shop; the shop can sell Coffee for IOTA.
balance: Balance<IOTA>,
}

  • COFFEE: A token with the ability drop, allowing it to be destroyed when no longer needed.
  • CoffeeShop: Represents a shop that has an ID, a treasury for minting coffee points, and a balance in IOTA.

5. Initialization with OneTimeWitness (OTW)

In Move, the init function plays a critical role during the module's lifecycle, executing only once at the moment of module publication. The init function initializes the CoffeeShop by creating the COFFEE token and defining the shop’s treasury cap and balance.

A One-Time Witness (OTW) is a unique type in Move, specifically designed to ensure that certain actions within a module can only occur once, providing security for the system's setup.

Function Arguments

  • otw: COFFEE: A token witness that allows the creation of the COFFEE tokens. It ensures that the token setup can only happen once during initialization.
  • ctx: &mut TxContext: The transaction context, which provides necessary data about the current transaction, including the sender and other blockchain-related information
    fun init(otw: COFFEE, ctx: &mut TxContext) {
let (coffee_points, metadata) = coin::create_currency(
otw, 0, b"COFFEE", b"Coffee Point",
b"Buy 4 coffees and get 1 free",
std::option::none(),
ctx
);

iota::transfer::public_freeze_object(metadata);
iota::transfer::share_object(CoffeeShop {
coffee_points,
id: object::new(ctx),
balance: balance::zero(),
});
}

Here, you use the OneTimeWitness (OTW) pattern, where COFFEE is used as a witness to authorize the creation of a new currency called COFFEE. The shop’s balance starts at zero.

6. Buying a Coffee (Token Transfer)

The buy_coffee function enables customers to purchase coffee using IOTA tokens. In this process, you will also apply control flow using assertions to ensure the customer has sufficient funds. If the required conditions are met, the transaction proceeds; otherwise, an error is triggered, making this a crucial aspect of program logic.

Function Arguments

  • app: &mut CoffeeShop: A mutable reference to the CoffeeShop struct, which holds the shop's token treasury and balance.
  • payment: Coin<IOTA>: The amount of IOTA tokens that the customer pays for the coffee. This is a Coin type representing IOTA currency.
  • ctx: &mut TxContext: The transaction context that handles information about the sender and the blockchain transaction.
    /// shop and the customer gets a free coffee after 4 purchases.
public fun buy_coffee(app: &mut CoffeeShop, payment: Coin<IOTA>, ctx: &mut TxContext) {
// Check if the customer has enough IOTA to pay for the coffee.
assert!(coin::value(&payment) > COFFEE_PRICE, EIncorrectAmount);

let token = token::mint(&mut app.coffee_points, 1, ctx);
let request = token::transfer(token, ctx.sender(), ctx);

token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx);
coin::put(&mut app.balance, payment);
iota::event::emit(CoffeePurchased {})
}

This function performs key tasks like checking the customer’s balance, minting 1 new COFFEE token, transferring it to the customer, and confirming the transaction.

7. Control Flow with Assertions

Move uses assertions (assert!) to enforce logical conditions during execution. If an assertion fails, it aborts the function with the defined error code.

        assert!(coin::value(&payment) > COFFEE_PRICE, EIncorrectAmount);

In this example, you will ensure the customer has enough IOTA to buy coffee. If not, it returns the EIncorrectAmount error.

8. Claiming a Free Coffee (Burning Tokens)

After purchasing 4 coffees, customers can claim a free coffee by burning their COFFEE points. In Move, burning tokens permanently removes them from circulation, ensuring they can’t be reused. This process is vital in controlling token supply and enforcing business logic in applications.

Function Arguments

  • app: &mut CoffeeShop: A mutable reference to the CoffeeShop struct, holding the shop’s treasury cap and token information.
  • points: Token<COFFEE>: The COFFEE tokens that the customer is using to claim a free coffee. The token value must equal 4 for the transaction to proceed.
  • ctx: &mut TxContext: The transaction context, which includes details about the sender and the blockchain transaction.
    /// coffee shop and the customer gets a free coffee after 4 purchases. The
/// `COFFEE` tokens are spent.
public fun claim_free(app: &mut CoffeeShop, points: Token<COFFEE>, ctx: &mut TxContext) {
// Check if the customer has enough `COFFEE` points to claim a free one.
assert!(token::value(&points) == 4, EIncorrectAmount);

// While we could use `burn`, spend illustrates another way of doing this
let request = token::spend(points, ctx);
token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx);
iota::event::emit(CoffeePurchased {})
}

This function ensures the customer has exactly 4 points, spends them, and emits a purchase event, marking the reward for loyalty.

9. Transferring Tokens

The transfer function enables customers to transfer their COFFEE tokens to others, but with a 1-point fee deducted as commission. This demonstrates how Move can enforce rules and fees during token transfers, ensuring a fair system of exchange.

Function Arguments

  • app: &mut CoffeeShop: A mutable reference to the CoffeeShop struct, which includes the shop's treasury and balance information.
  • mut points: Token<COFFEE>: The COFFEE tokens being transferred. The token value is mutable because 1 token will be deducted as a transfer fee.
  • recipient: address: The recipient's blockchain address to which the tokens will be transferred.
  • ctx: &mut TxContext: The transaction context that includes details about the sender and manages interaction with the blockchain.
    /// `COFFEE` point for the transfer.
public fun transfer(
app: &mut CoffeeShop,
mut points: Token<COFFEE>,
recipient: address,
ctx: &mut TxContext
) {
assert!(token::value(&points) > 1, ENotEnoughPoints);
let commission = token::split(&mut points, 1, ctx);
let request = token::transfer(points, recipient, ctx);

token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx);
token::burn(&mut app.coffee_points, commission);
}
}

The function first checks if the user has enough points, splits off 1 point as the transfer fee, sends the remaining points to the recipient, and burns the 1-point commission from the system's treasury, removing it from circulation.

Full Contract

Click to view code
// Copyright (c) Mysten Labs, Inc.
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0

/// This example illustrates how to use the `Token` without a `TokenPolicy`. And
/// only rely on `TreasuryCap` for minting and burning tokens.
module coffee_shop::coffee {
use iota::tx_context::sender;
use iota::coin::{Self, TreasuryCap, Coin};
use iota::balance::{Self, Balance};
use iota::token::{Self, Token};
use iota::iota::IOTA;

/// Error code for incorrect amount.
const EIncorrectAmount: u64 = 0;
/// Trying to claim a free coffee without enough points.
/// Or trying to transfer but not enough points to pay the commission.
const ENotEnoughPoints: u64 = 1;

/// 10 IOTA for a coffee.
const COFFEE_PRICE: u64 = 10_000_000_000;

/// OTW for the Token.
public struct COFFEE has drop {}

/// The shop that sells Coffee and allows to buy a Coffee if the customer
/// has 10 COFFEE points.
public struct CoffeeShop has key {
id: UID,
/// The treasury cap for the `COFFEE` points.
coffee_points: TreasuryCap<COFFEE>,
/// The IOTA balance of the shop; the shop can sell Coffee for IOTA.
balance: Balance<IOTA>,
}

/// Event marking that a Coffee was purchased; transaction sender serves as
/// the customer ID.
public struct CoffeePurchased has copy, store, drop {}

// Create and share the `CoffeeShop` object.
fun init(otw: COFFEE, ctx: &mut TxContext) {
let (coffee_points, metadata) = coin::create_currency(
otw, 0, b"COFFEE", b"Coffee Point",
b"Buy 4 coffees and get 1 free",
std::option::none(),
ctx
);

iota::transfer::public_freeze_object(metadata);
iota::transfer::share_object(CoffeeShop {
coffee_points,
id: object::new(ctx),
balance: balance::zero(),
});
}

/// Buy a coffee from the shop. Emitted event is tracked by the real coffee
/// shop and the customer gets a free coffee after 4 purchases.
public fun buy_coffee(app: &mut CoffeeShop, payment: Coin<IOTA>, ctx: &mut TxContext) {
// Check if the customer has enough IOTA to pay for the coffee.
assert!(coin::value(&payment) > COFFEE_PRICE, EIncorrectAmount);

let token = token::mint(&mut app.coffee_points, 1, ctx);
let request = token::transfer(token, sender(ctx), ctx);

token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx);
coin::put(&mut app.balance, payment);
iota::event::emit(CoffeePurchased {})
}

/// Claim a free coffee from the shop. Emitted event is tracked by the real
/// coffee shop and the customer gets a free coffee after 4 purchases. The
/// `COFFEE` tokens are spent.
public fun claim_free(app: &mut CoffeeShop, points: Token<COFFEE>, ctx: &mut TxContext) {
// Check if the customer has enough `COFFEE` points to claim a free one.
assert!(token::value(&points) == 4, EIncorrectAmount);

// While you could use `burn`, spend illustrates another way of doing this
let request = token::spend(points, ctx);
token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx);
iota::event::emit(CoffeePurchased {})
}

/// you will allow transfer of `COFFEE` points to other customers but you charge 1
/// `COFFEE` point for the transfer.
public fun transfer(
app: &mut CoffeeShop,
mut points: Token<COFFEE>,
recipient: address,
ctx: &mut TxContext
) {
assert!(token::value(&points) > 1, ENotEnoughPoints);
let commission = token::split(&mut points, 1, ctx);
let request = token::transfer(points, recipient, ctx);

token::confirm_with_treasury_cap(&mut app.coffee_points, request, ctx);
token::burn(&mut app.coffee_points, commission);
}
}

10. Building your Package

You can use the iota move build command to build Move packages in the working directory, coffee_shop in this case.

iota move build

11. Publish the coffee_shop package by running the following command:

iota client publish

The console will respond with the transaction effects. You should pay attention to the created objects to retrieve the object IDs.

Conclusion

In this tutorial, you explored the basics of Move programming by building a simple Coffee Shop application. You learned how to import modules, define constants, create structs, mint and burn tokens, and manage control flow with assertions. With this foundational knowledge, you can now begin experimenting with more advanced Move features and expand your decentralized applications. Happy coding!