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:
-
Every trade operation in the Kiosk requires a TransferPolicy resolution giving creators control over how their assets are traded.
-
"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.
-
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.
-
Changes to the TransferPolicy are instant and global.
Practical set of guarantees:
-
While an item is traded, it can not be modified or taken.
-
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.
-
Any Rule can be removed at any time.
-
Any extension can be disabled at any time.
-
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:
- Place and take items
- List items for sale
- Add and remove Extensions
- Withdraw profits from sales
- Borrow and mutate owned assets
- Access full arsenal of trading tools (eg auctions, lotteries, collection bidding etc)
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:
- Set any rules for trades
- Set multiple ways ("tracks") of rules
- Enable or disable trades at any moment with a policy
- Enforce policies (eg royalties) on all trades
- Perform a primary sale of their assets through a Kiosk
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 theTransferPolicy
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 TransferRequest
s 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:
- A
Rule
Witness type which uniquely identifies the Rule - A
Config
type which is stored in theTransferPolicy
and is used to configure the Rule (eg a fee amount) - An
add
function which adds the Rule to theTransferPolicy
- must be performed by theTransferPolicyCap
holder - An actionable function which adds the receipt into the
TransferRequest
and potentially adds to theTransferPolicy
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:
- RuleWitness struct
- Config struct stored in the TransferPolicy
- "set" function which adds the Rule to the TP
- 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:
- Item ID
- Amount paid (SUI)
- 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 pricekiosk::delist
- remove an existing listingkiosk::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 assetkiosk::borrow_mut
- returns a mutable reference to the assetkiosk::borrow_val
- a PTB-friendly version ofborrow_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
- An extension can only be installed by an explicit call in the extension module.
- Kiosk Owner can revoke permissions of an extension at any time by calling the
disable
function. - A disabled extension can be re-enabled at any time by the Kiosk Owner by calling the
enable
function. - 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:
Bit | Decimal | Permission |
---|---|---|
0000 | 0 | No permissions |
0001 | 1 | Extension can place |
0010 | 2 | Extension can place and lock |
0011 | 3 | Extension 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 placelock<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 Feature | Default Event | Custom Feature | Custom Event |
---|---|---|---|
list | ItemListed<T> | list_with_purchase_cap | None |
delist | ItemDelisted<T> | return_purchase_cap | None |
purchase | ItemPurchased<T> | purchase_with_cap | None |
When building custom events, it is important to keep in mind, that
sender
andtimestamp
are default meta-properties of all emitted events. So adding asender
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 TransferRequest
s.
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
TransferRequest
s, 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
- take
- list - change state to Listed
- list with PurchaseCap - change state to Listed Exclusively
- borrow
- borrow_mut
- borrow_val
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
- list - change state to Listed
- list with PurchaseCap - change state to Listed Exclusively
- borrow
- borrow_mut
- borrow_val
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
- purchase with PurchaseCap - move the asset out of the Kiosk
- return PurchaseCap - return to the previous state: Placed or Locked
- borrow
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);