Creating Loyalty Token
You can use the IOTA Closed-Loop Token standard to create tokens that are valid only within a specific service. For example, an airline might grant tokens to frequent flyers that they can use to purchase tickets or upgrades.
In this guide, you'll learn how to create a loyalty token that users can use to make purchases in a digital gift shop.
Overview
The following example demonstrates how to create a loyalty token using the Closed-Loop Token standard.
As the administrator, you would send LOYALTY
tokens to your service's users as a reward for their loyalty.
The example includes a GiftShop
where holders can spend LOYALTY
tokens to buy Gift
items.
Module: examples::loyalty
The examples::loyalty
module, found in the loyalty.move
source file, contains the code to create the loyalty token.
The module defines a one-time witness (OTW)
that creates the coin named LOYALTY
.
This coin possesses only the drop
ability and has no fields.
These characteristics ensure the LOYALTY
type has a single instance.
/// The OTW for the Token / Coin.
public struct LOYALTY has drop {}
Initialization Function
The module's init
function uses the LOYALTY
OTW to create the token.
Remember that all init
functions run only once at the package publish event.
The initializer function calls create_currency
using the LOYALTY
type defined earlier.
It also sets up a policy by sending both the policy capability
and the treasury capability to the address associated with the publish event.
The holder of these transferable capabilities can mint new LOYALTY
tokens and modify their policies.
fun init(otw: LOYALTY, ctx: &mut TxContext) {
let (treasury_cap, coin_metadata) = coin::create_currency(
otw,
0, // no decimals
b"LOY", // symbol
b"Loyalty Token", // name
b"Token for Loyalty", // description
option::none(), // url
ctx
);
let (mut policy, policy_cap) = token::new_policy(&treasury_cap, ctx);
// but we constrain spend by this shop:
token::add_rule_for_action<LOYALTY, GiftShop>(
&mut policy,
&policy_cap,
token::spend_action(),
ctx
);
token::share_policy(policy);
transfer::public_freeze_object(coin_metadata);
transfer::public_transfer(policy_cap, tx_context::sender(ctx));
transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
}
Minting Function: reward_user
The reward_user
function allows the holder of the TreasuryCap
to mint new loyalty tokens and send them to specified addresses.
It uses the token::mint
function
to create the tokens and token::transfer
to deliver them to the intended recipients.
public fun reward_user(
cap: &mut TreasuryCap<LOYALTY>,
amount: u64,
recipient: address,
ctx: &mut TxContext
) {
let token = token::mint(cap, amount, ctx);
let req = token::transfer(token, recipient, ctx);
token::confirm_with_treasury_cap(cap, req, ctx);
}
Redeeming Tokens: buy_a_gift
Finally, the module includes a buy_a_gift
function to handle the redemption of LOYALTY
tokens for Gift
items.
This function ensures that the gift's price matches the number of loyalty tokens spent.
It uses the token::spend
function to manage the treasury bookkeeping.
public fun buy_a_gift(
token: Token<LOYALTY>,
ctx: &mut TxContext
): (Gift, ActionRequest<LOYALTY>) {
assert!(token::value(&token) == GIFT_PRICE, EIncorrectAmount);
let gift = Gift { id: object::new(ctx) };
let mut req = token::spend(token, ctx);
// only required because we've set this rule
token::add_approval(GiftShop {}, &mut req, ctx);
(gift, req)
}
}
Full Source Code
For a complete view of the module, you can review the full source code below.
Click to view the complete source code
// Copyright (c) Mysten Labs, Inc.
// Modifications Copyright (c) 2024 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
/// This module illustrates a Closed Loop Loyalty Token. The `Token` is sent to
/// users as a reward for their loyalty by the application Admin. The `Token`
/// can be used to buy a `Gift` in the shop.
///
/// Actions:
/// - spend - spend the token in the shop
module examples::loyalty {
use iota::coin::{Self, TreasuryCap};
use iota::token::{Self, ActionRequest, Token};
/// Token amount does not match the `GIFT_PRICE`.
const EIncorrectAmount: u64 = 0;
/// The price for the `Gift`.
const GIFT_PRICE: u64 = 10;
/// The OTW for the Token / Coin.
public struct LOYALTY has drop {}
/// This is the Rule requirement for the `GiftShop`. The Rules don't need
/// to be separate applications, some rules make sense to be part of the
/// application itself, like this one.
public struct GiftShop has drop {}
/// The Gift object - can be purchased for 10 tokens.
public struct Gift has key, store {
id: UID
}
// Create a new LOYALTY currency, create a `TokenPolicy` for it and allow
// everyone to spend `Token`s if they were `reward`ed.
fun init(otw: LOYALTY, ctx: &mut TxContext) {
let (treasury_cap, coin_metadata) = coin::create_currency(
otw,
0, // no decimals
b"LOY", // symbol
b"Loyalty Token", // name
b"Token for Loyalty", // description
option::none(), // url
ctx
);
let (mut policy, policy_cap) = token::new_policy(&treasury_cap, ctx);
// but we constrain spend by this shop:
token::add_rule_for_action<LOYALTY, GiftShop>(
&mut policy,
&policy_cap,
token::spend_action(),
ctx
);
token::share_policy(policy);
transfer::public_freeze_object(coin_metadata);
transfer::public_transfer(policy_cap, tx_context::sender(ctx));
transfer::public_transfer(treasury_cap, tx_context::sender(ctx));
}
/// Handy function to reward users. Can be called by the application admin
/// to reward users for their loyalty :)
///
/// `Mint` is available to the holder of the `TreasuryCap` by default and
/// hence does not need to be confirmed; however, the `transfer` action
/// does require a confirmation and can be confirmed with `TreasuryCap`.
public fun reward_user(
cap: &mut TreasuryCap<LOYALTY>,
amount: u64,
recipient: address,
ctx: &mut TxContext
) {
let token = token::mint(cap, amount, ctx);
let req = token::transfer(token, recipient, ctx);
token::confirm_with_treasury_cap(cap, req, ctx);
}
/// Buy a gift for 10 tokens. The `Gift` is received, and the `Token` is
/// spent (stored in the `ActionRequest`'s `burned_balance` field).
public fun buy_a_gift(
token: Token<LOYALTY>,
ctx: &mut TxContext
): (Gift, ActionRequest<LOYALTY>) {
assert!(token::value(&token) == GIFT_PRICE, EIncorrectAmount);
let gift = Gift { id: object::new(ctx) };
let mut req = token::spend(token, ctx);
// only required because we've set this rule
token::add_approval(GiftShop {}, &mut req, ctx);
(gift, req)
}
}