What is Sui Kiosk

Kiosk is a decentralized system for commerce applications on Sui. It consists of “Kiosks” - shared objects owned by individual parties which store assets and allow listing them for sale as well as utilize custom trading functionality - for example, an Auction. While being highly decentralized, Kiosk provides a set of strong guarantees:

  • Kiosk Owners retain ownership of their assets to the moment of purchase;
  • Creators set custom “policies” - sets of rules applied to every trade (eg pay Royalty fee, do some arbitrary action X);
  • Marketplaces can index events emitted by the Kiosk and subscribe to a single feed for on-chain asset trading;

Practically, Kiosk is a part of the Sui Framework, and it is native to the system and available to everyone “out of the box”.

Overview

TBD

Philosophy

TBD

Guarantees

The system comes with a set of guarantees that are enforced by the smart contracts. These guarantees are:

  1. Every trade operation in the Kiosk requires a TransferPolicy resolution giving creators control over how their assets are traded.

  2. "True Ownership" - Kiosk Owner is the only party that can take, list, borrow and modify assets in their Kiosk. No other party can do this. Similarly to how single owner objects work on Sui.

  3. Strong Policy (eg Royalty) enforcement is an option for creators which can be enabled or disabled at any time affecting all trades on the platform.

  4. Changes to the TransferPolicy are instant and global.


Practical set of guarantees:

  1. While an item is traded, it can not be modified or taken.

  2. While PurchaseCap exists, an item is locked and can not be taken or modified unless the PCap is returned or used to perform a trade.

  3. Any Rule can be removed at any time.

  4. Any extension can be disabled at any time.

  5. Extension state is always accessible to the extension.

Roles

As an ecosystem Kiosk defines 3 major roles and sets the rules of interaction between them. The roles are: Creator, Kiosk Owner, and Buyer. Each of the roles has a set of advantages and limitations - the former is the available functionality and the latter is the set of constraints that allows other roles to function.

In this section we go through all of the roles in detail and describe their interactions.

Kiosk Owner

Anyone on the network can create a Kiosk and use it to store and trade assets. The Kiosk Owner is the owner of the KioskOwnerCap - a special object that grants full access to a single Kiosk.

Kiosk Owner can:

Kiosk provides a set of guarantees to the Kiosk Owner:

  • Safety - no other party can access items or profits stored in the Kiosk
  • Liquidity - the Kiosk can be used to trade assets globally on the network
  • Ownership - traded items never leave the Kiosk and are owned by the Kiosk Owner until the sale is complete

Creator

Creator is a party that creates and controls the TransferPolicy for a single type. For example, the authors of SuiFrens are the Creators of the SuiFren<Capy> type and act as creators in the Kiosk ecosystem. Creators set the policy, but they may also be the first sellers of their assets through a Kiosk.

Creator can:

All of the above is effective immediately and globally.

Creator can not:

  • Take or modify items stored in someone else's Kiosk
  • Restrict taking items from Kiosks if the "locking" rule was not set in the policy

Buyer

Buyer is a party that purchases (or - more general - receives) items from Kiosks, anyone on the network can be a Buyer (and, for example, a Kiosk Owner at the same time).

Benefits:

  • Buyers get access to global liquidity and can get the best offer
  • Buyers can place bids on collections through their Kiosks
  • Most of the actions performed in Kiosks are free (gas-less) for Buyers

Responsibilities:

  • Buyer is the party that pays the fees if they're set in the policy
  • Buyer must follow the rules set by creators or a transaction won't succeed

Guarantees:

  • When using a custom trading logic such as an Auction, the items are guaranteed to be unchanged until the trade is complete

Transfer Policy

To understand the Kiosk we first need to go through the Transfer Policy. transfer_policy is a library located in the Sui Framework; it allows creating a non-discardable message - called TransferRequest - and the destination for this message - TransferPolicy.

Introduction

Imagine a tax system implementation: multiple merchants process payments and buyers need to pay "VAT". The tax is calculated based on the price of the purchased item, and different categories of items have different tax rates.

In most of the countries, the VAT is first collected by the merchant. The merchant must keep track of the tax paid for each item and pay the collected taxes to the tax authority in the end of the period.

To create a system like this without extra overhead on the merchants' side (the buyer pays the tax directly) we could issue a "receipt" for each purchase which would contain the information about the item type and the price paid. The buyer would then need to pay the VAT directly to the tax authority based on the receipt.

Move language has a primitive which allows creating a non-discardable message called "Hot Potato". Once created, the Hot Potato can only be consumed by a specific module (or object), and if it is not consumed, the transaction will fail.

Applied to this scenario, the "receipt" would be a Hot Potato, forcing the buyer to do something with it - pay the tax - before the transaction can be finalized.

Transfer Request

The TransferRequest struct and the matching TransferPolicy object address this problem. If a merchant creates a TransferRequest upon each purchase, and the TransferPolicy is configured to enforce the tax policy, the buyer will not be able to finalize the purchase the item until the tax is paid.

This is a strong guarantee, because the TransferRequest is a non-discardable struct - "Hot Potato" - and unless it finds "its home", the transaction won't succeed. The TransferPolicy is the "home" for the TransferRequest and it can be configured by the authority to require certain conditions to be met for the TransferRequest to be accepted.

Because the system is designed for commerce, the TransferRequest has the most commonly used fields, all of which are set at creation and can't be changed, such as:

  • paid (the amount of SUI paid for the item)
  • item (the ID of the item being transferred)
  • from (the ID of the source the item is being sold from)

Additionally, thanks to the Move's type system, the TransferRequest is issued per type. This means that the TransferRequest for a "Phone" is different from the TransferRequest for a "Car". This allows to enforce different rules for different types of items.

Example: Merchant

A module implementing the merchant logic for the "Phone" type could look like this:

/// An example of a module on Sui which sells "Phones".
module commerce::merchant {
    use sui::transfer_policy::{Self, TransferRequest};
    use tax::vat::VAT;

    /// A single "Phone" - a Sui Object.
    struct Phone has key, store { /* ... */ }

    /// A price of a single "Phone".
    const PHONE_PRICE: u64 = 10_000_000_000;

    /// Some merchant ID (usually represented by a Sui Object)
    const MERCHANT_ID: address = 0xBEEF;

    /// The merchant is selling phones, the buyer only pays to the merchant the
    /// price of the phone, and the tax is paid separately and directly to the
    /// tax authority. `VAT` type is imported and can only be resolved by the
    /// authority defining this type.
    public fun buy_phone(/* pass a coin */): (Phone, TransferRequest<VAT>) {

        let phone = Phone { /* ... */ };

        // Generate new `TransferRequest` for the `VAT` type, specify the ID
        // of the `Phone` object, the price of the `Phone` and the ID of the
        // merchant.
        let request = transfer_policy::new_request<VAT>(
            object::id(&phone),
            PHONE_PRICE,
            object::id_from_address(MERCHANT_ID),
        );

        (phone, request)
    }
}

Transfer Policy

A TransferRequest can only be matched in a TransferPolicy object. The match happens based on the type of the TransferRequest and the TransferPolicy. If the TransferRequest is for the "Phone" type, it can only be matched by the TransferPolicy for the "Phone" type.

In the example above the TransferRequest is for the "VAT" type, so it can only be matched by the TransferPolicy for the "VAT" type.

TransferPolicy<T> is a Sui Object which can be created for any type by the Publisher of this type.

/// An example of a TransferPolicy setup. Better be done via PTBs and not as a
/// published module. The code below is for illustration purposes only.
module tax::vat {
    use sui::tx_context::{sender, TxContext};
    use sui::transfer_policy::{Self, TransferPolicy};

    /// The authority defines the `VAT` type. It is never initialized and only
    /// used to create and resolve `TransferRequest`s.
    struct VAT has drop {}

    /// The publisher creates and shares a `TransferPolicy` for the `VAT` type.
    public fun create_policy(pub: &Publisher, ctx: &mut TxContext) {
        let (policy, policy_cap) = transfer_policy::new_policy<VAT>(pub, ctx);

        sui::transfer::public_share_object(policy);
        sui::transfer::public_transfer(policy_cap, sender(ctx));
    }

    /// Can be called directly in the `TransferPolicy` module; does not need a
    /// custom implementation. This code is for illustration purposes only.
    public fun confirm_request(
        policy: &TransferPolicy<VAT>, request: TransferRequest<VAT>
    ) {
        transfer_policy::confirm_request<VAT>(policy, request);
    }

    /// Using the OTW (VAT), create the Publisher object and transfer it to the
    /// transaction sender.
    fun init(otw: VAT, ctx: &mut TxContext) {
        sui::package::claim_and_keep(otw, ctx);
    }
}

Rules

The TransferPolicy does not require any action from the user by default; it confirms TransferRequests and therefore unblocks a transaction. However, if we were to implement the VAT example further and allow the VAT to collect fees for every "merchant transaction" we need to introduce "Rules".

"Receipts" in TransferRequest

TransferRequest features the receipts field which is a VecSet of TypeName. When the request is "confirmed" via the confirm_request call, the receipts are compared against the TransferPolicy.rules, and if "receipts" don't match the "rules", the request can not be confirmed, and the transaction aborts.

In the default scenario, the rules are empty and receipts are too, so the matching is trivial and the request is confirmed.

module sui::transfer_policy {
    // ... skipped ...

    struct TransferRequest<phantom T> {
        // ... other fields omitted ...

        /// Collected Receipts. Used to verify that all of the rules
        /// were followed and `TransferRequest` can be confirmed.
        receipts: VecSet<TypeName>
    }

    // ... skipped ...

    struct TransferPolicy<phantom T> has key, store {
        // ... other fields omitted ...

        /// Set of types of attached rules - used to verify `receipts` when
        /// a `TransferRequest` is received in `confirm_request` function.
        ///
        /// Additionally provides a way to look up currently attached Rules.
        rules: VecSet<TypeName>
    }

    // ...
}

Rules and Receipts

A Rule is a way to request an additional action from the user before the request can be confirmed. For example, if we want to implement a "VAT" module that would collect fees for every "merchant transaction", we need to introduce a Rule that would allow the VAT to collect fees. The way for us to know that the VAT is paid is to add a "receipt" to the TransferRequest.

A Rule added to the TransferPolicy requires a matching Receipt in the TransferRequest. The match is performed based on the Rule type.

Rule Implementation

A rule is a module that implements the "Rule" functionality in the transfer_policy module - it needs to provide 3 main components:

  1. A Rule Witness type which uniquely identifies the Rule
  2. A Config type which is stored in the TransferPolicy and is used to configure the Rule (eg a fee amount)
  3. An add function which adds the Rule to the TransferPolicy - must be performed by the TransferPolicyCap holder
  4. An actionable function which adds the receipt into the TransferRequest and potentially adds to the TransferPolicy balance if the functionality involves some monetary transaction.

Guide: Writing Rules

When an item is purchased in a Kiosk, a TransferRequest hot-potato is created, and the only way to resolve it and unblock the transaction is to confirm the request in the matching TransferPolicy. This guide explains how the TransferPolicy works and how new rules can be implemented and added into a policy.

Basics

An item of a type T can only be traded in Kiosks if the TransferPolicy for T exists and available to the buyer. This requirement is based on a simple fact that the TransferRequest issued on purchase must be resolved in a matching TransferPolicy and if there isn't one or buyer can't access it, the transaction will fail.

This system was designed to give maximum freedom and flexibility for creators: by taking the transfer policy logic out of the trading primitive we make sure that the policies can be set only by creators, and as long as the trading primitive is used, enforcements are under their control. Effectively creators became closer to the trading ecosystem and got an important and solid role in the process.

Architecture

By default, a single TransferPolicy does not enforce anything - if a buyer attempts to confirm their TransferRequest, it will go through. However, the system allows setting so-called "Rules". Their logic is simple: someone can publish a new rule module, for example "fixed fee", and let it be "added" or "set" for the TransferPolicy. Once the Rule is added, TransferRequest needs to collect a TransferReceipt marking that the requiement specified in the Rule was completed.

[TODO]

  • Once a Rule is added to the TransferPolicy, every TransferRequest going to the policy must have a matching Receipt

Rule structure: Dummy

Every rule would follow the same structure and implement required types:

  1. RuleWitness struct
  2. Config struct stored in the TransferPolicy
  3. "set" function which adds the Rule to the TP
  4. an action function which adds a Receipt to the TransferRequest

Important: there's no need to implement "unset" - any rule can be removed at any time as defined in the TransferPolicy module and guaranteed by the set of constraints on the rule Config (store + drop)

module examples::dummy_rule {
    use sui::coin::Coin;
    use sui::sui::SUI;
    use sui::transfer_policy::{
        Self as policy,
        TransferPolicy,
        TransferPolicyCap,
        TransferRequest
    };

    /// The Rule Witness; has no fields and is used as a
    /// static authorization method for the rule.
    struct Rule has drop {}

    /// Configuration struct with any fields (as long as it
    /// has `drop`). Managed by the Rule module.
    struct Config has store, drop {}

    /// Function that adds a Rule to the `TransferPolicy`.
    /// Requires `TransferPolicyCap` to make sure the rules are
    /// added only by the publisher of T.
    public fun add<T>(
        policy: &mut TransferPolicy<T>,
        cap: &TransferPolicyCap<T>
    ) {
        policy::add_rule(Rule {}, policy, cap, Config {})
    }

    /// Action function - perform a certain action (any, really)
    /// and pass in the `TransferRequest` so it gets the Receipt.
    /// Receipt is a Rule Witness, so there's no way to create
    /// it anywhere else but in this module.
    ///
    /// This example also illustrates that Rules can add Coin<SUI>
    /// to the balance of the TransferPolicy allowing creators to
    /// collect fees.
    public fun pay<T>(
        policy: &mut TransferPolicy<T>,
        request: &mut TransferRequest<T>,
        payment: Coin<SUI>
    ) {
        policy::add_to_balance(Rule {}, policy, payment);
        policy::add_receipt(Rule {}, request);
    }
}

This module contains no configuration and requires a Coin<SUI> of any value (even "0"), so it's easy to imagine that every buyer would create a zero Coin and pass it to get the Receipt. The only thing this Rule module is good for is illustration and a skeleton. Goes without saying but this code should never be used in production.

Reading the Request: Royalty

To implement a percentage-based fee (a very common scenario - royalty fee), a Rule module needs to know the price for which an item was purchased. And the TransferRequest contains some information which can be used in this and other scenarios:

  1. Item ID
  2. Amount paid (SUI)
  3. From ID - the object which was used for selling (eg Kiosk)

To provide access to these fields, the sui::transfer_policy module has a set of getter functions which are available to anyone: "paid()", "item()" and "from()"

module examples::royalty_rule {
    // skipping dependencies
    const MAX_BP: u16 = 10_000;

    struct Rule has drop {}

    /// In this implementation Rule has a configuration - `amount_bp`
    /// which is the percentage of the `paid` in basis points.
    struct Config has store, drop { amount_bp: u16 }

    /// When a Rule is added, configuration details are specified
    public fun add<T>(
        policy: &mut TransferPolicy<T>,
        cap: &TransferPolicyCap<T>,
        amount_bp: u16
    ) {
        assert!(amount_bp <= MAX_BP, 0);
        policy::add_rule(Rule {}, policy, cap, Config { amount_bp })
    }

    /// To get the Receipt, the buyer must call this function and pay
    /// the required amount; the amount is calculated dynamically and
    /// it is more convenient to use a mutable reference
    public fun pay<T>(
        policy: &mut TransferPolicy<T>,
        request: &mut TransferRequest<T>,
        payment: &mut Coin<SUI>,
        ctx: &mut TxContext
    ) {
        // using the getter to read the paid amount
        let paid = policy::paid(request);
        let config: &Config = policy::get_rule(Rule {}, policy);
        let amount = (((paid as u128) * (config.amount_bp as u128) / MAX_BP) as u64);
        assert!(coin::value(payment) >= amount, EInsufficientAmount);

        let fee = coin::split(payment, amount, ctx);
        policy::add_to_balance(Rule {}, policy, fee);
        policy::add_receipt(Rule {}, request)
    }
}

Time is also Money

Rules don't need to be only for payments and fees. Some might allow trading before or after a certain time. Since Rules are not standardized and can use anything, developers can encode logic around using any objects.

module examples::time_rule {
    // skipping some dependencies
    use sui::clock::{Self, Clock};

    struct Rule has drop {}
    struct Config has store, drop { start_time: u64 }

    /// Start time is yet to come
    const ETooSoon: u64 = 0;

    /// Add a Rule that enables purchases after a certain time
    public fun add<T>(/* skip default fields */, start_time: u64) {
        policy::add_rule(Rule {}, policy, cap, Config { start_time })
    }

    /// Pass in the Clock and prove that current time value is higher
    /// than the `start_time`
    public fun confirm_time<T>(
        policy: &TransferPolicy<T>,
        request: &mut TransferRequest<T>,
        clock: &Clock
    ) {
        let config: &Config = policy::get_rule(Rule {}, policy)
        assert!(clock::timestamp_ms(clock) >= config.start_time, ETooSoon);
        policy::add_receipt(Rule {}, request)
    }
}

Generalizing approach: Witness policy

Sui Move has two main ways for authorizing an action: static - by using the Witness pattern, and dynamic - via the Capability pattern. With a small addition of type parameters to the Rule, it is possible to create a generic Rule which will not only vary by configuration but also by the type of the Rule.

module examples::witness_rule {
    // skipping dependencies

    /// Rule is either not set or the Witness does not match the expectation
    const ERuleNotSet: u64 = 0;

    /// This Rule requires a witness of type W, see the implementation
    struct Rule<phantom W> has drop {}
    struct Config has store, drop {}

    /// No special arguments are required to set this Rule, but the
    /// publisher now needs to specify a Witness type
    public fun add<T, W>(/* .... */) {
        policy::add_rule(Rule<W> {}, policy, cap, Config {})
    }

    /// To confirm the action, buyer needs to pass in a witness
    /// which should be acquired either by calling some function or
    /// integrated into a more specific hook of a marketplace /
    /// trading module
    public fun confirm<T, W>(
        _: W,
        policy: &TransferPolicy<T>,
        request: &mut TransferRequest<T>
    ) {
        assert!(policy::has_rule<T, Rule<W>>(policy), ERuleNotSet);
        policy::add_receipt(Rule<W> {}, request)
    }
}

The "witness_rule" is very generic and can be used to require a custom Witness depending on the settings. It is a simple and yet a powerful way to link a custom marketplace / trading logic to the TransferPolicy. With a slight modification, the rule can be turned into a generic Capability requirement (basically any object, even a TransferPolicy for a different type or a TransferRequest - no limit to what could be done).

module examples::capability_rule {
    // skipping dependencies

    /// Changing the type parameter name for better readability
    struct Rule<phantom Cap> has drop {}
    struct Config {}

    /// Absolutely identical to the witness setting
    public fun add<T, Cap>(/* ... */) {
        policy::add_rule(Rule<Cap> {}, policy, cap, Config {})
    }

    /// Almost the same with the Witness requirement, only now we
    /// require a reference to the type.
    public fun confirm<T, Cap>(
        cap: &Cap,
        /* ... */
    ) {
        assert!(policy::has_rule<T, Rule<Cap>>(policy), ERuleNotSet);
        policy::add_receipt(Rule<Cap> {}, request)
    }
}

Multiple Transfer Policies

While most of the scenarios imply having a single TransferPolicy for a type, it is possible to create multiple policies for different purposes. For example, a default policy for "VAT" would require everyone to use it. However, if a person is leaving the country, they can get their VAT refunded; is it possible to resolve? The answer is yes, and it is possible to do it without changing the default policy.

To do so, a second TransferPolicy for the same type can be issued and usually wrapped into a custom object to implement the logic. For example, a "TaxFreePolicy" object can be created and used to ignore - not pay - VAT. The object will store another TransferPolicy which can be accessed only if the buyer shows a valid "Passport" object. The inner policy may contain no rules, therefore not requiring any fees to be paid.

Kiosk

Kiosk is a simple yet highly customizable tool for building and interacting with commerce applications on chain. In its simplest form, a Kiosk is an object that stores users' assets and allows trading them for a price. Just like an old-school real-world kiosk - a single owner, a single place, different items for sale.

Almost all of the functions of the Kiosk are only available to the Kiosk Owner - the exception would be the Purchase function, and it's derivative - PurchaseCap, both of which require the Kiosk Owner to first list the asset for sale.

In this section we'll cover the basic functions of Kiosk, show how to use them in different environments (CLI, TypeScript), and how to extend the Kiosk functionality with custom logic.

Open a Kiosk

To use a Kiosk, the user needs to create it and have the KioskOwnerCap that matches the Kiosk object. Once created, all of the features of the Kiosk are available to the owner.

Default setup

Anyone can create a new Kiosk in a single transaction by calling the kiosk::default function. It will create and share a Kiosk and transfer the KioskOwnerCap to the transaction sender.

Example Kiosk SDK

import { createKioskAndShare } from '@mysten/kiosk';

let tx = new TransactionBuilder();
let kioskOwnerCap = createKioskAndShare(tx);

tx.transferObjects([ kioskOwnerCap ], tx.pure(sender, 'address'));

Example PTB

let tx = new TransactionBuilder();
tx.moveCall({
    target: '0x2::kiosk::default'
});

Example CLI

sui client call \
    --package 0x2 \
    --module kiosk \
    --function default \
    --gas-budget 1000000000

Advanced usage

For more advanced use cases, when you want to choose the storage model or perform an action right away, you can use a PTB-friendly function kiosk::new.

Kiosk is intended to be shared and choosing a different storage model (eg "owned") would lead to Kiosk not being fully functional and not available for other users. Make sure you know what you're doing.

Example Kiosk SDK

import { createKiosk } from '@mysten/kiosk';

let tx = new TransactionBuilder();
let [kiosk, kioskOwnerCap] = createKiosk(tx);

tx.transferObjects([ kioskOwnerCap ], tx.pure(sender, 'address'));
tx.moveCall({
    target: '0x2::transfer::public_share_object',
    arguments: [ kiosk ],
    typeArguments: '0x2::kiosk::Kiosk'
})

Example PTB

let tx = new TransactionBuilder();
let [kiosk, kioskOwnerCap] = tx.moveCall({
    target: '0x2::kiosk::new'
});

tx.transferObjects([ kioskOwnerCap ], tx.pure(sender, 'address'));
tx.moveCall({
    target: '0x2::transfer::public_share_object',
    arguments: [ kiosk ],
    typeArguments: '0x2::kiosk::Kiosk'
})

Example CLI

Sui CLI does not support PTBs and transaction chaining yet. You can use the kiosk::default function instead.

Place and Take

Kiosk owner can place any assets into their Kiosk, placed assets can be taken by the owner if they're not listed.

There's no limitations to which assets can be placed into the Kiosk, however, it does not guarantee that they will be tradable - that depends on whether there's a TransferPolicy for the type. See the Purchase section for more details.

Placing an item into the Kiosk

To place an item to the Kiosk, the owner needs to call the sui::kiosk::place function on the Kiosk object and pass the KioskOwnerCap and the Item as arguments.

ITEM_TYPE in the examples below is the full type of the item.

Example Kiosk SDK

import { place } from '@mysten/kiosk';

let tx = new TransactionBuilder();

let itemArg = tx.object('<ID>');
let kioskArg = tx.object('<ID>');
let kioskOwnerCapArg = tx.object('<ID>');

place(tx, '<ITEM_TYPE>', kioskArg, kioskOwnerCapArg, item);

Example PTB

let tx = new TransactionBuilder();

let itemArg = tx.object('<ID>');
let kioskArg = tx.object('<ID>');
let kioskOwnerCapArg = tx.object('<ID>');

tx.moveCall({
    target: '0x2::kiosk::place',
    arguments: [ kioskArg, kioskOwnerCapArg, itemArg ],
    typeArguments: [ '<ITEM_TYPE>' ]
})

Example CLI

sui client call \
    --package 0x2 \
    --module kiosk \
    --function place \
    --args "<KIOSK_ID>" "<CAP_ID>" "<ITEM_ID>" \
    --type-args "<ITEM_TYPE>" \
    --gas-budget 1000000000

Taking an item from the Kiosk

To take an item from the Kiosk, the owner needs to call the sui::kiosk::take function on the Kiosk object and pass the KioskOwnerCap and ID of the item as arguments.

ITEM_TYPE in the examples below is the full type of the item.

Example Kiosk SDK

import { take } from '@mysten/kiosk';

let tx = new TransactionBuilder();

let itemId = tx.pure('<ITEM_ID>', 'address');
let kioskArg = tx.object('<ID>');
let kioskOwnerCapArg = tx.object('<ID>');

let item = take('<ITEM_TYPE>', kioskArg, kioskOwnerCapArg, itemId);

tx.transferObjects([ item ], tx.pure(sender, 'address'));

Example PTB

let tx = new TransactionBuilder();

let itemId = tx.pure('<ITEM_ID>', 'address');
let kioskArg = tx.object('<ID>');
let kioskOwnerCapArg = tx.object('<ID>');

let item = tx.moveCall({
    target: '0x2::kiosk::take',
    arguments: [ kioskArg, kioskOwnerCapArg, itemId ],
    typeArguments: [ '<ITEM_TYPE>' ]
});

Example CLI

The kiosk::take function is built to be PTB friendly and returns the asset, and CLI does not support transaction chaining yet.

Locking

Some policies may require assets to never leave Kiosk (eg in a strong royalty enforcement scenario), and for that Kiosk has a locking mechanism.

Locking is similar to placing with an exception, that a locked asset can not be taken out of the Kiosk.

An asset can be locked in a Kiosk by calling the sui::kiosk::lock function. To make sure that the asset can be eventually unlocked, the call requires a TransferPolicy to exist.

Lock an asset in a Kiosk

Similar to place, lock call requires the KioskOwnerCap and the Item as arguments, but also requires the TransferPolicy to be shown.

<ITEM_TYPE> in the examples below is the full type of the asset.

Example Kiosk SDK

import { lock } from '@mysten/kiosk';

const tx = new TransactionBuilder();

let kioskArg = tx.object('<ID>');
let kioskOwnerCapArg = tx.object('<ID>');
let itemArg = tx.object('<ID>');
let transferPolicyArg = tx.object('<ID>');

lock(tx, '<ITEM_TYPE>', kioskArg, kioskOwnerCapArg, transferPolicyArg, itemArg);

Example PTB

const tx = new TransactionBuilder();

let kioskArg = tx.object('<ID>');
let kioskOwnerCapArg = tx.object('<ID>');
let itemArg = tx.object('<ID>');
let transferPolicyArg = tx.object('<ID>');

tx.moveCall({
    target: '0x2::kiosk::lock',
    arguments: [ kioskArg, kioskOwnerCapArg, transferPolicyArg, itemArg ],
    typeArguments: [ '<ITEM_TYPE>' ]
});

Example CLI

sui client call \
    --package 0x2 \
    --module kiosk \
    --function lock \
    --args "<KIOSK_ID>" "<CAP_ID>" "<TRANSFER_POLICY_ID>" "<ITEM_ID>" \
    --type-args "<ITEM_TYPE>" \
    --gas-budget 1000000000

List and Delist

Kiosk comes with a basic trading functionality. The Kiosk Owner can list assets for sale and buyers can discover and purchase them. Listing functionality is available in Kiosk by default, and features 3 main functions:

  • kiosk::list - list an asset for sale for a fixed price
  • kiosk::delist - remove an existing listing
  • kiosk::purchase - purchase an asset that is listed for sale

Listing an asset

Kiosk Owner can list any asset asset that is stored in their Kiosk. To do so they need to call the kiosk::list function, specify the asset they're willing to put on sale, and the price they're willing to sell it for.

All listings are in SUI at the moment.

When an item is listed, a kiosk::ItemListed event is emitted. The event contains the Kiosk ID, Item ID, type of the Item and the price it was listed for.

Example Kiosk SDK

import { list } from '@mysten/kiosk';

let tx = new TransactionBlock();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');
let itemId = tx.pure('<ID>', 'address');
let itemType = 'ITEM_TYPE';
let price = '<price>'; // in MIST (1 SUI = 10^9 MIST)

list(tx, itemType, kioskArg, capArg, itemId, price);

Example PTB

let tx = new TransactionBlock();

let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');
let itemId = tx.pure('<ID>', 'address');
let itemType = 'ITEM_TYPE';
let priceArg = tx.pure('<price>', 'u64'); // in MIST (1 SUI = 10^9 MIST)

tx.moveCall({
    target: '0x2::kiosk::list',
    arguments: [ kioskArg, capArg, itemId, priceArg ],
    typeArguments: [ itemType ]
});

Example CLI

sui client call \
    --package 0x2 \
    --module kiosk \
    --function list \
    --args "<KIOSK_ID>" "<CAP_ID>" "<ITEM_ID>" "<PRICE>" \
    --type-args "ITEM_TYPE" \
    --gas-budget 1000000000

Delisting an asset

Kiosk Owner can delist any currently listed asset. To delist an asset they need to call the kiosk::delist function, and specify the item they're willing to delist.

Delisting is a "negative-gas" operation, meaning that the Kiosk Owner will be refunded for the gas they spent on listing the item.

When an item is delisted, a kiosk::ItemDelisted event is emitted. The event contains the Kiosk ID, Item ID and the type of the Item.

Example Kiosk SDK

import { delist } from '@mysten/kiosk';

let tx = new TransactionBlock();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');
let itemId = tx.pure('<ID>', 'address');
let itemType = 'ITEM_TYPE';

delist(tx, itemType, kioskArg, capArg, itemId);

Example PTB

let tx = new TransactionBlock();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');
let itemId = tx.pure('<ID>', 'address');
let itemType = 'ITEM_TYPE';

tx.moveCall({
    target: '0x2::kiosk::delist',
    arguments: [ kioskArg, capArg, itemId ],
    typeArguments: [ itemType ]
});

Example CLI

sui client call \
    --package 0x2 \
    --module kiosk \
    --function delist \
    --args "<KIOSK_ID>" "<CAP_ID>" "<ITEM_ID>" \
    --type-args "ITEM_TYPE" \
    --gas-budget 1000000000

More on the topic

A listed item can be purchased by anyone on the network, to see the purchase flow, check out the Purchase section. To learn more about asset states and what can be done with a listed item, see the Asset States section.

Purchase

A listed item can be purchased by anyone on the network. The buyer should call the kiosk::purchase function, specify the item they're willing to purchase and pay the price set by the Kiosk Owner.

Currently listed items can be discovered via the kiosk::ItemListed event.

The purchase function returns the purchased Asset and the TransferRequest for this its type which needs to be resolved in a matching TransferPolicy. See the TransferPolicy section for more details.

Kiosk SDK

Borrowing

An asset placed or locked in a Kiosk can be accessed by the Kiosk Owner without taking it from it. The Kiosk Owner can always borrow the asset immutably, however mutable borrow depends on the asset state - a listed item can not be modified. The functions at the Kiosk Owner's disposal are:

  • kiosk::borrow - returns an immutable reference to the asset
  • kiosk::borrow_mut - returns a mutable reference to the asset
  • kiosk::borrow_val - a PTB-friendly version of borrow_mut - allows to take an asset and return it in the same transaction

Immutable borrow

An asset can always be immutably borrowed from a Kiosk. Borrowing is performed via the kiosk::borrow function, however, it is not possible to use references within a PTB, so to access the immutable borrow, a published module (function) is required.

Example Move

module examples::immutable_borrow
    use sui::object::ID;
    use sui::kiosk::{Self, Kiosk, KioskOwnerCap};

    public fun immutable_borrow_example<T>(self: &Kiosk, cap: &KioskOwnerCap, item_id: ID): &T {
        kiosk::borrow(self, cap, item_id)
    }
}

Mutable borrow with borrow_mut

An asset can be mutably borrowed from a Kiosk if it is not listed. Borrowing is performed via the kiosk::borrow_mut function, however, it is not possible to use references within a PTB, so to access the mutable borrow, a published module (function) is required.

Example Move

module examples::mutable_borrow
    use sui::object::ID;
    use sui::kiosk::{Self, Kiosk, KioskOwnerCap};

    public fun mutable_borrow_example<T>(
        self: &mut Kiosk, cap: &KioskOwnerCap, item_id: ID
    ): &mut T {
        kiosk::borrow_mut(self, cap, item_id)
    }
}

Mutable borrow with borrow_val (PTB)

A PTB-friendly borrowing is available with the kiosk::borrow_val function. It allows to take an asset and return it in the same transaction. To make sure an asset is returned, the function "obliges" the caller with a Hot Potato.

Example Kiosk SDK

Kiosk SDK gives a handy function for the borrowing logic usable within a PTB: borrowValue (and returnValue).

import { borrowValue, returnValue } from '@sui/kiosk-sdk';

let tx = new TransactionBuilder();
let itemType = 'ITEM_TYPE';
let itemId = tx.pure('<ITEM_ID>', 'address');
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');

let [item, promise] = borrowValue(tx, itemType, kioskArg, capArg, itemId);

// freely mutate or reference the `item`
// any calls are available as long as they take a reference
// `returnValue` must be explicitly called

returnValue(tx, itemType, kioskArg, item, promise);

Example PTB

let tx = new TransactionBuilder();

let itemType = 'ITEM_TYPE';
let itemId = tx.pure('<ITEM_ID>', 'address');
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');

let [item, promise] = tx.moveCall({
    target: '0x2::kiosk::borrow_val',
    arguments: [ kioskArg, capArg, itemId ],
    typeArguments: [ itemType ],
});

// freely mutate or reference the `item`
// any calls are available as long as they take a reference
// `returnValue` must be explicitly called

tx.moveCall({
    target: '0x2::kiosk::return_val',
    arguments: [ kioskArg, item, promise ],
    typeArguments: [ itemType ],
});

Withdrawing Profits

Whevener an item is purchased, profits from the sale are stored in the Kiosk. The Kiosk Owner can withdraw these profits at any time by calling the kiosk::withdraw function.

Examples

The function is simple, however, due to it being PTB friendly, it is not currently supported in the CLI environment.

Example Kiosk SDK

import { withdrawFromKiosk } from '@mysten/kiosk';

let tx = new TransactionBlock();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');

// The amount can be `null` to withdraw everything or a specific amount
let amount = '<amount>';
let withdrawAll = null;

let coin = withdrawFromKiosk(tx, kioskArg, capArg, amount);

Example PTB

let tx = new TransactionBlock();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');

// because the function uses an Option<u64> argument,
// constructing is a bit more complex
let amountArg = tx.moveCall({
    target: '0x1::option::some',
    arguments: [ tx.pure('<amount>', 'u64') ],
    typeArguments: [ 'u64' ],
});

// alternatively
let withdrawAllArg = tx.moveCall({
    target: '0x1::option::none',
    typeArguments: [ 'u64' ],
});

let coin = tx.moveCall({
    target: '0x2::kiosk::withdraw',
    arguments: [ kioskArg, capArg, amountArg ],
    typeArguments: [ 'u64' ],
});

Example CLI

Due to the function being PTB friendly, it is not currently supported in the CLI environment.

Purchase Cap

PurchaseCap tricks

Extensions

Kiosk Extensions are a way to extend the functionality of the Kiosk. They're represented as modules published on Sui and can be called or installed by the Kiosk Owner to provide additional functionality to the Kiosk. Not only do they allow for custom trading logic but also for minor improvements such as attaching a name to the Kiosk.

See more examples and implementation details in the Extensions section.

Kiosk Extensions

Extensions are a way to extend the functionality of Kiosk while keeping the core functionality intact. They are a way to add new features to the Kiosk without having to modify the core code or move the assets elsewhere.

Types of Extensions

There are two types of extensions:

Simple Extensions

Ones that do not require Extensions API to function. They usually serve the purpose of adding custom metadata to the Kiosk or wrapping / working with exising objects such as Kiosk or KioskOwnerCap. An example of an extension that does not require the API is the Personal Kiosk extension.

Permissioned Extensions

"Permissioned" extensions use the Extensions API to perform actions in the Kiosk. They usually imply interaction with a third party and provide guarantees for the storage access (preventing the malicious actions from the seller).

Simple Extensions

Some extensions may be implemented without the "more advanced" Kiosk Extensions API. To understand the approaches to building extensions of this kind, let's look at the tools that are available to the extension developer.

Available tools

UID access via the uid_mut

Kiosk, like any object on Sui, has an id: UID field, which allows this object to not just be uniquely identified but also carry custom dynamic fields and dynamic object fields. The Kiosk itself is built around dynamic fields and features like place and list are built around dynamic object fields.

The uid_mut_as_owner function

Kiosk can carry additional dynamic fields and dynamic object fields. The uid_mut_as_owner function allows the Kiosk Owner to mutably access the UID of the Kiosk object and use it to add or remove custom fields.

Function signature:

kiosk::uid_mut_as_owner(self: &mut Kiosk, cap: &KioskOwnerCap): &mut UID

The public uid getter

Anyone can read the uid of the Kiosk. This allows third party modules read the fields of the Kiosk if they're allowed to do so (TODO: Custom Dynamic Field Keys). Therefore enabling the "Object Capability" and other patterns.

Extension ideas

Given that the Kiosk Owner can attach custom dynamic fields to Kiosk, and anyone can then read them (but not modify), we can use this to implement simple extensions. For example, a "Kiosk Name" extension: the Kiosk Owner can set a name for the Kiosk, attach it as a dynamic field, and make it readable by anyone.

We used a similar approach in the Personal Kiosk Extension.

module examples::kiosk_name_ext {
    use std::string::String;
    use std::option::{Self, Option};
    use sui::dynamic_field as df;
    use sui::kiosk::{Self, Kiosk, KioskOwnerCap};

    /// The dynamic field key for the Kiosk Name Extension
    struct KioskName has store, drop {}

    /// Add a name to the Kiosk (in this implementation can be called only once)
    public fun add(self: &mut Kiosk, cap: &KioskOwnerCap, name: String) {
        let uid_mut = kiosk::uid_mut_as_owner(self, cap);
        df::add(uid_mut, KioskName {}, name)
    }

    /// Try read the name of the Kiosk - if set - return Some(String), if not - None
    public fun name(self: &Kiosk): Option<String> {
        if (df::exists_(kiosk::uid(self), KioskName {}) {
            option::some(*df::borrow(kiosk::uid(self), KioskName {}))
        } else {
            option::none()
        }
    }
}

Extensions API

Just having access to the uid is often not enough to build an extension due to the security limitations. Only Kiosk Owner has full access to the uid, which means that an extension involving a third party would require the Kiosk Owner to be involved in every step of the process.

Not only the access to storage is limited and constrained but also the permissions of the extension. In the default setup, no party can place or lock items in a Kiosk without its Owner's consent. So some cases such as "collection bidding" (I offer X SUI for any item in this collection) will require the Kiosk Owner to approve the bid.

kiosk_extension module

To address these concerns and provide more guarantees over the storage access, we created the kiosk_extension module. It provides a set of functions which enable the extension developer to perform certain actions in the Kiosk without the Kiosk Owner's involvement and have a guarantee that the storage of the extension is not tampered with.

module example::my_extension {
    use sui::kiosk_extension;

    // ...
}

Extension Lifecycle

  1. An extension can only be installed by an explicit call in the extension module.
  2. Kiosk Owner can revoke permissions of an extension at any time by calling the disable function.
  3. A disabled extension can be re-enabled at any time by the Kiosk Owner by calling the enable function.
  4. Extension can be removed only in if the Extension Storage is empty, i.e. all items are removed.

Adding an Extension

For the extension to function, it first needs to be installed by the Kiosk owner. To achieve that, an extension needs to implement the add function which will be called by the Kiosk owner and will request all necessary permissions.

Implementing add function

The signature of the kiosk_extension::add function requires the extension witness making it impossible to install an extension without an explicit implementation provided by the extension. The following example shows how to implement the add function for an extension that requires the place permission:

module examples::letterbox_ext {
    // ... dependencies

    /// The expected set of permissions for extension. It requires `place`.
    const PERMISSIONS: u128 = 1;

    /// The Witness struct used to identify and authorize the extension.
    struct Extension has drop {}

    /// Install the Mallbox extension into the Kiosk.
    public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) {
        kiosk_extension::add(Extension {}, kiosk, cap, PERMISSIONS, ctx)
    }
}

Extension Permissions

Extensions can request permissions from the Kiosk Owner on installation. Permissions follow the all or nothing principle. If the Kiosk Owner adds an extension it gets all of the requested permissions; if the Kiosk Owner then disables an extension, all of its permissions are revoked.

Structure

Permissions are represented as a u128 integer storing a bitmap. Each of the bits corresponds to a permission, the first bit is the least significant bit. The following table lists all permissions and their corresponding bit:

BitDecimalPermission
00000No permissions
00011Extension can place
00102Extension can place and lock
00113Extension can place and lock

Currently, Kiosk has only 2 permissions: place (1st bit) and lock and place (2nd bit). The rest of the bits are reserved for future use.

Using permissions in the add function

It's a good practice to define a constant containing permissions of the extension:

module examples::letterbox_ext {
    // ... dependencies

    /// The expected set of permissions for extension. It requires `place`.
    const PERMISSIONS: u128 = 1;

    /// The Witness struct used to identify and authorize the extension.
    struct Extension has drop {}

    /// Install the Mallbox extension into the Kiosk and request `place` permission.
    public fun add(kiosk: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext) {
        kiosk_extension::add(Extension {}, kiosk, cap, PERMISSIONS, ctx)
    }
}

Accessing protected functions

If Extension requested permissions and was added and if it's not disabled, it can access protected functions. The following example shows how to access the place function:

module examples::letterbox_ext {
    // ...

    /// Emitted when trying to place an item without permissions.
    const ENotEnoughPermissions: u64 = 1;

    /// Place a letter into the Kiosk without the KioskOwnerCap.
    public fun place(kiosk: &mut Kiosk, letter: Letter, policy: &TransferPolicy<T>) {
        assert!(kiosk_extension::can_place<Extension>(kiosk), ENotEnoughPermissions)

        kiosk_extension::place(Extension {}, kiosk, letter, policy)
    }
}

Currently, two functions are available:

  • place<Ext, T>(Ext, &mut Kiosk, T, &TransferPolicy<T>) - similar to place
  • lock<Ext, T>(Ext, &mut Kiosk, T, &TransferPolicy<T>) - similar to lock

Checking permissions

The can_place<Ext>(kiosk: &Kiosk): bool function can be used to check if the extension has the place permission. The can_lock<Ext>(kiosk: &Kiosk): bool function can be used to check if the extension has the lock permission. Both functions make sure that the Extension is enabled, so an explicit check for that is not needed.

Extension Storage

Every extension gets its isolated storage which can be accessed only by the extension module (providing the Extension Witness). It's a Bag. Once an extension is installed, it can use the storage to store its data. Ideally, the storage should be managed in a way that allows the extension to be removed from the Kiosk if there are no active trades or other activities happening at the moment.

The storage is always available to the extension if it is installed. Kiosk Owner can't access the storage of the extension if the logic for it is not implemented.

Accessing the storage

An installed extension can access the storage mutably or immutably using one of the following functions:

  • storage(_ext: Extension {}, kiosk: &Kiosk): Bag: returns a reference to the storage of the extension. Can be used to read the storage.
  • storage_mut(_ext: Extension {}, kiosk: &mut Kiosk): &mut Bag: returns a mutable reference to the storage of the extension. Can be used to read and write to the storage.

Disabling and Removing

Any extension can be disabled by the Kiosk Owner at any time. This will revoke all permissions of the extension and prevent it from performing any actions in the Kiosk. The extension can be re-enabled at any time by the Kiosk Owner.

Disabling an extension does not remove it from the Kiosk. An installed Extension has access to its storage until completely removed from the Kiosk.

Disabling an extension

The disable<Ext>(kiosk: &mut Kiosk, cap: &KioskOwnerCap) function can be used to disable an extension. It will revoke all permissions of the extension and prevent it from performing any protected actions in the Kiosk.

Example PTB

let txb = new TransactionBuilder();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');

txb.moveCall({
    target: '0x2::kiosk_extension::disable',
    arguments: [ kioskArg, capArg ],
    typeArguments: '<letter_box_package>::letterbox_ext::Extension'
});

Removing an extension

An extension can be removed only if the storage is empty. The remove<Ext>(kiosk: &mut Kiosk, cap: &KioskOwnerCap) function can be used to remove an extension. It will remove the extension, unpack the extension storage and configuration and rebate the storage cost to the Kiosk Owner. Can only be performed by the Kiosk Owner.

The call will fail if the storage is not empty.

Example PTB

let txb = new TransactionBuilder();
let kioskArg = tx.object('<ID>');
let capArg = tx.object('<ID>');

txb.moveCall({
    target: '0x2::kiosk_extension::remove',
    arguments: [ kioskArg, capArg ],
    typeArguments: '<letter_box_package>::letterbox_ext::Extension'
});

Building with Kiosk

Kiosk comes with a simple trading functionality which consists of two functions: list (and a matching delist) and purchase. It emits events when an action happens which enables off-chain discovery of Kiosk activity.

An application utilizing Kiosk‘s default functions needs to be able to interact with Kiosks, listen to Kiosk events and update the interfaces accordingly and resolve TransferRequests on purchase. While an application creating custom logic for Kiosks needs to also build an interaction logic based on PurchaseCap’s and emit events to track its activity off-chain.

In this section we go through main areas which need to be covered for both default and custom flows and look into what’s necessary to build an application with Kiosk.

Demo Applications

Currently, there are two public demo applications for Kiosk: “Kiosk Demo” and “Kiosk CLI” both of which can be used as code references for building custom client applications.

Kiosk Demo

This demo application illustrates a simple list / purchase flow built with TypeScript, React and Kiosk SDK.

  • Live: interactive version is available at: https://sui-kiosk.vercel.app/
  • Code: the code is located in the Sui repository under the dapps/kiosk

Kiosk CLI

This application implements Kiosk functionality as a pure JavaScript, showing best practices for each of the actions.

  • Code: the code is located in the Sui repository under the dapps/kiosk-cli path

Future

More demo applications are coming, the flows that are not currently covered but will be are:

  • Creator demo - creating and setting up a TransferPolicy to enable asset trading in Kiosks
  • Marketplace demo - setting up a Marketplace with the upcoming Marketplace Extension

Indexing Events

Using Kiosk in a Module

Custom Events in Extensions

While the default Kiosk implementation for list-purchase flow always emits events, custom features covered in the purchase-cap section do not. And this is intentional - to keep the space available for custom events integration.

If we were to map default functionality of the Kiosk to analogous “custom” features, the map would look like:

Default FeatureDefault EventCustom FeatureCustom Event
listItemListed<T>list_with_purchase_capNone
delistItemDelisted<T>return_purchase_capNone
purchaseItemPurchased<T>purchase_with_capNone

When building custom events, it is important to keep in mind, that sender and timestamp are default meta-properties of all emitted events. So adding a sender field in an event is not necessary and even more - it increases the event emission price (which is typically paid by user).

Each of the PurchaseCap-enabled actions follows the general principle of the list-purchase flow, however there are no events emitted. An extension utilizing these functions should create and emit custom events.

The example below uses Extensions API, make sure to look through this section beforehand.

/// This module follows the default Kiosk functionality without anything added.
/// Practically the example does not have any value and serves the illustration 
/// purpose as well as a potential base for custom extensions
module examples::marketplace_ext {
	use sui::kiosk::{Self, Kiosk, KioskOwnerCap, PurchaseCap};
	use sui::tx_context::TxContext;
	use sui::object::{Self, ID};
	use sui::kiosk_extension;
	use sui::event;

	/// Trying to delist an item in someone else’s Kiosk
	const ENotOwner: u64 = 0;

	/// A custom extension flag
	struct Extension has drop {}

	// === Events ===

	/// A custom event for a new listing. We only emit Kiosk ID and the price,
	/// because other fields such as “sender” are already a part of the event.
	/// And the type `T` here allows filtering events by type.
	struct MarketItemListed<phantom T> has copy, drop {
		kiosk: ID,
		item_id: ID,
		price: u64
	}

	/// A custom event for delisting. We only emit Kiosk ID and Item ID, 
	/// because the rest can be identified on the indexing side 
	struct MarketItemDelisted<phantom T> has copy, drop {
		kiosk: ID,
		item_id: ID
	}

    // Similarly, a purchase event can be implemented
    // struct MarketItemPurchased<phantom T> has copy, drop { ... }

	/// Installs the extension into user’s Kiosk.
	public fun add(
		self: &mut Kiosk, cap: &KioskOwnerCap, ctx: &mut TxContext
	) {
		kiosk_extension::add(Extension {}, self, cap, 0, ctx)
	}

	/// Custom list function which emits an event specific to the Extension
	public fun list<T: key + store>(
		self: &mut Kiosk, 
		cap: &KioskOwnerCap, 
		item_id: ID, 
		price: u64, 
		ctx: &mut TxContext
	) {
		// omitting the purchase cap and storage parts
		// see Extensions API for details

		event::emit(MarketItemListed<T> {
    		kiosk: object::id(&self),
    		item_id,
    		price,
		})
	}
	
	/// Custom delist function which emits 
	public fun delist<T: key + store>(
		self: &mut Kiosk, cap: &KioskOwnerCap, item_id: ID,
	) {
		assert!(kiosk::has_access(self, cap), ENotOwner);

		event::emit(MarketItemDelisted<T> {
			kiosk: object::id(&self),
			item_id
		})
	}

	// 
}

Mysten Kiosk

Kiosk is a flexible base for building commerce-related applications on top of it. And as such it's not providing any specifics in the implementation. However, we offer a set of extensions and rules which cover the most common use cases.

Mysten Kiosk package contains rules and extensions and aims to be a Swiss army knife for on-chain commerce. The package is being developed and upgraded regularly to make sure there's a solution for most of the common use cases.

Repository and Code

Code is located in the MystenLabs/apps repository. It features the sources and instructions on how to add them to your project on both mainnet and testnet environments.

Extensions

  • Personal Kiosk Extension makes the Kiosk non-transferrable and locks it to the owner. This allows creators to check whether their assets are traded in a user-owned Kiosk or whether it's a custom Kiosk-based solution (which may allow Kiosk trading - a way to escape policy enforcement).

Rules

  • Royalty Rule allows creators to set a royalty fee for every trade of their assets. The fee is paid to the creator on every trade. Not only does it add a percentage-based fee to the trade, but it also allows setting a min_price - a minimum fee paid for the asset (for trades below a certain threshold).

  • Kiosk Lock Rule allows creators to lock their assets in a Kiosk disabling the "take" operation.

  • Personal Kiosk Rule allows creators to enable trades only between personal Kiosks. Enforces the Personal Kiosk Extension on the buyer's side.

  • Floor Price Rule allows creators to set a minimum price for their assets in a Kiosk. The rule enforces the minimum price setting on the seller's side.

SDK

Mysten Kiosk package is fully supported by the Kiosk SDK @mysten/kiosk, which provides handy functions to perform actions in a Kiosk and resolve TransferRequests.

Appendix

This section contains additional, mostly technical materials which can be used as a reference when using or building an application utilizing Kiosk.

Appendix A: Glossary

  • Kiosk - a single Kiosk object that stores assets and their states, and profits from sales; protects the contents, the only party that can access and change the state is Kiosk Owner.

  • Kiosk Owner - a party that owns the KioskOwnerCap - can be an application (represented as an object) or a single account

  • TransferPolicy - an object that authorizes TransferRequests, by default requires no actions but can be modified by adding Rules. TransferPolicy is controlled by the Creator, and can only receive payments and approve TransferRequests.

  • Transfer Request - a temporary non-discardable struct (Hot Potato) created on every purchase operation in Kiosk. Must be resolved at a matching TransferPolicy for the transaction to succeed. If a policy has Rules, each of the rules must add a Rule Receipt to the TransferRequest before the confirmation.

  • Creator - a party that owns the TransferPolicyCap - an object that grants full access to the TransferPolicy. Can be both an application (represented as an object) and a single account.

  • Rule - a single requirement in a TransferPolicy represented as a module with a unique witness type and a function to “satisfy the rule” and add a receipt in the TransferRequest.

  • Rule Receipt - a “stamp” put into a TransferRequest by presenting a witness (instance of a droppable struct); receipts in the TransferRequest are compared against Rules added to the TransferPolicy, and if they match, a request can be confirmed.

Appendix B: Asset States in Kiosk

An asset in Kiosk can be in one of the following states:

  • Placed
  • Locked (special case of Placed)
  • Listed
  • Listed Exclusively

Placed

Asset is put into the Kiosk by the Kiosk Owner using the kiosk::place function. An owner can perform any available action on an item in this state.

Available actions

Check state

To check that asset is in the right state, the caller can use is_placed function, however, to make sure that the asset is not locked, we need to check !is_locked.

let is_placed = kiosk::is_placed<T>(&Kiosk, ItemID) && !kiosk::is_locked<T>(&Kiosk, ItemID);

Locked

Asset can also be placed and locked in a Kiosk using the kiosk::lock function. Unlike place, locking mechanic disables taking.

Available actions

Check state

Kiosk has a built in is_locked function to check if the item is locked.

let is_locked = kiosk::is_locked<T>(&Kiosk, ItemID);

Listed

A placed or a locked item can be listed using the list function. The asset then becomes publicly available for purchase.

While listed, an asset can not be modified.

Available actions

  • purchase - move the asset out of the Kiosk
  • delist - return to the previous state: Placed or Locked
  • borrow

Check state

To check if the item is listed, use is_listed function.

let is_listed = kiosk::is_listed<T>(&Kiosk, ItemID)

Listed Exclusively

When an asset is listed using the list_with_purchase_cap, it gets “listed exclusively” state. While in this state, an asset is available for purchase to the owner of the PurchaseCap, and cannot be delisted unless the PurchaseCap is returned.

While listed exclusively, an asset can not be modified.

Available actions

Check state

To check if an asset is listed exclusively, use is_listed_exclusively function.

let is_listed_exclusively = kiosk::is_listed_exclusively<T>(&Kiosk, ItemID);

Appendix C: Extension States