Define a View Function
This how-to demonstrates how to mark a Move function as a read-only view function using the #[view] attribute. The examples come from a package with four modules: a leaderboard that ranks player scores, an auction, a counter and a generic, time-locked vault.
Read State From a View
These examples come from the leaderboard module, which stores player scores in a shared object.
- Start from a module with some state to read. Here a shared
Leaderboardholds a vector ofScoreEntryvalues.
/// A shared leaderboard, with entries ranked from highest score to lowest.
public struct Leaderboard has key {
id: UID,
admin: address,
entries: vector<ScoreEntry>,
}
/// A single player's standing on the leaderboard.
public struct ScoreEntry has copy, drop, store {
player: address,
score: u64,
}
- Annotate a
publicfunction with#[view]. A view takes its object by immutable reference (&Leaderboard) and returns a value — this one returns the entry count.
/// Returns the number of recorded scores.
#[view]
public fun total_entries(board: &Leaderboard): u64 {
board.entries.length()
}
- A view may take additional immutable references and return an
Option. This one looks up the entry for a givenplayer, returningnonewhen the player has no score.
/// Returns the entry recorded for `player`, or `none` if they have no score.
#[view]
public fun score_of(board: &Leaderboard, player: &address): Option<ScoreEntry> {
let mut i = 0;
while (i < board.entries.length()) {
let entry = &board.entries[i];
if (&entry.player == player) return option::some(*entry);
i = i + 1;
};
option::none()
}
- A view may return a vector of values, such as
vector<ScoreEntry>—ScoreEntryis a non-object struct withcopyanddrop, so it may be returned by value.
/// Returns every recorded score, ranked from highest to lowest.
#[view]
public fun all_scores(board: &Leaderboard): vector<ScoreEntry> {
board.entries
}
Return a Reference From a View
A view may return an immutable reference; mutable references are rejected. This lets a view hand back a borrow into the state it read, instead of a copy. The auction module keeps its bids inline and stores a per-bidder tally as dynamic fields.
- Define the auction and its bids.
/// A shared English auction for a single lot.
public struct Auction has key {
id: UID,
/// Human-readable description of the lot on sale.
lot: String,
seller: address,
/// Unix timestamp (ms) after which no further bids are accepted.
ends_at: u64,
/// The highest amount bid so far; zero until the first bid.
highest_bid: u64,
/// Every bid placed, in the order received.
bids: vector<Bid>,
}
/// A single bid placed on the auction.
public struct Bid has copy, drop, store {
bidder: address,
/// Bid amount in the smallest currency unit.
amount: u64,
}
/// Dynamic-field key for a bidder's running tally.
public struct BidderKey has copy, drop, store {
bidder: address,
}
/// A bidder's activity, stored as a dynamic field keyed by their address.
public struct BidderTally has store {
/// How many bids this bidder has placed.
count: u64,
/// This bidder's highest bid so far.
highest: u64,
}
- Return an immutable reference into a field of the object received by reference.
/// Returns an immutable reference to the whole list of bids.
///
/// A view may return an immutable reference into an object it received by
/// reference; only mutable references are disallowed.
#[view]
public fun bids(auction: &Auction): &vector<Bid> {
&auction.bids
}
- Return a reference to a value stored as a dynamic field.
/// Returns an immutable reference to a bidder's tally, stored as a dynamic field.
#[view]
public fun tally_of(auction: &Auction, bidder: address): &BidderTally {
dynamic_field::borrow(&auction.id, BidderKey { bidder })
}
- Return a tuple that mixes a value and a reference.
/// Returns the bid's amount together with a reference to the bid.
///
/// A view may return a tuple, and that tuple may contain immutable references.
#[view]
public fun bid_with_amount(auction: &Auction, index: u64): (u64, &Bid) {
let bid = &auction.bids[index];
(bid.amount, bid)
}
Write a Generic View
A view can be generic. A type parameter used only behind a reference may be unconstrained; a type parameter passed or returned by value must have copy or drop. The vault module wraps a single generic value behind a time lock.
- Define a generic container.
/// A shared, generic vault holding a single value of type `T`.
///
/// The contents stay locked until `unlock_at`, after which only `beneficiary`
/// may release them.
public struct Vault<T: store> has key {
id: UID,
item: T,
/// Unix timestamp (ms) before which the vault cannot be unlocked.
unlock_at: u64,
/// The only address permitted to unlock the vault.
beneficiary: address,
}
- Return an immutable reference to the stored item.
Tneeds no extra constraint because it is only accessed behind a reference.
/// Returns an immutable reference to the stored item.
///
/// The view itself places no constraint on `T`: a type parameter used only
/// behind a reference may be unconstrained.
#[view]
public fun item<T: store>(vault: &Vault<T>): &T {
&vault.item
}
- A view on a generic type can read more than one object. This one also takes the shared
Clockby immutable reference to report whether the time lock has elapsed.
/// Returns true if the time lock has elapsed as of `clock`.
#[view]
public fun is_unlockable<T: store>(vault: &Vault<T>, clock: &Clock): bool {
clock.timestamp_ms() >= vault.unlock_at
}
What Not to Annotate
A function that mutates state cannot be a view. submit_score takes &mut Leaderboard and re-ranks its entries, so it is a regular function — adding #[view] to it would be a compile error.
/// Record a score for `player`, keeping the leaderboard ranked highest-first.
///
/// If the player already has a score it is replaced and re-ranked.
public fun submit_score(board: &mut Leaderboard, player: address, score: u64, ctx: &TxContext) {
assert!(ctx.sender() == board.admin, ENotAnAdmin);
// Drop any existing entry for this player so the new score is the only one.
let mut i = 0;
while (i < board.entries.length()) {
if (board.entries[i].player == player) {
board.entries.remove(i);
break
};
i = i + 1;
};
// Insert ahead of the first entry with a strictly lower score, keeping the
// vector sorted in descending order.
let mut pos = 0;
while (pos < board.entries.length() && board.entries[pos].score >= score) {
pos = pos + 1;
};
board.entries.insert(ScoreEntry { player, score }, pos);
}
A view also cannot take an object by value. vault::unlock consumes the Vault to release its contents, so it too must be a regular function.
/// Unlock the vault and return the stored item.
///
/// Aborts if the time lock has not elapsed, or if the sender is not the
/// `beneficiary`.
public fun unlock<T: store>(vault: Vault<T>, clock: &Clock, ctx: &TxContext): T {
assert!(clock.timestamp_ms() >= vault.unlock_at, EStillLocked);
assert!(ctx.sender() == vault.beneficiary, ENotAllowed);
let Vault { id, item, unlock_at: _, beneficiary: _ } = vault;
id.delete();
item
}
For the full list of what a view function may and may not do, see the view functions reference. To call the views you defined from off-chain, see Call a View Function.
Full Example Code
The complete leaderboard module:
// Copyright (c) 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
module view_functions::leaderboard;
#[error(code = 0)]
const ENotAnAdmin: vector<u8> = b"Only admin allowed.";
/// A shared leaderboard, with entries ranked from highest score to lowest.
public struct Leaderboard has key {
id: UID,
admin: address,
entries: vector<ScoreEntry>,
}
/// A single player's standing on the leaderboard.
public struct ScoreEntry has copy, drop, store {
player: address,
score: u64,
}
/// Create and share a new, empty leaderboard.
public fun create(ctx: &mut TxContext) {
transfer::share_object(Leaderboard {
id: object::new(ctx),
admin: ctx.sender(),
entries: vector[],
});
}
/// Record a score for `player`, keeping the leaderboard ranked highest-first.
///
/// If the player already has a score it is replaced and re-ranked.
public fun submit_score(board: &mut Leaderboard, player: address, score: u64, ctx: &TxContext) {
assert!(ctx.sender() == board.admin, ENotAnAdmin);
// Drop any existing entry for this player so the new score is the only one.
let mut i = 0;
while (i < board.entries.length()) {
if (board.entries[i].player == player) {
board.entries.remove(i);
break
};
i = i + 1;
};
// Insert ahead of the first entry with a strictly lower score, keeping the
// vector sorted in descending order.
let mut pos = 0;
while (pos < board.entries.length() && board.entries[pos].score >= score) {
pos = pos + 1;
};
board.entries.insert(ScoreEntry { player, score }, pos);
}
/// Returns the number of recorded scores.
#[view]
public fun total_entries(board: &Leaderboard): u64 {
board.entries.length()
}
/// Returns the entry recorded for `player`, or `none` if they have no score.
#[view]
public fun score_of(board: &Leaderboard, player: &address): Option<ScoreEntry> {
let mut i = 0;
while (i < board.entries.length()) {
let entry = &board.entries[i];
if (&entry.player == player) return option::some(*entry);
i = i + 1;
};
option::none()
}
/// Returns every recorded score, ranked from highest to lowest.
#[view]
public fun all_scores(board: &Leaderboard): vector<ScoreEntry> {
board.entries
}
/// Returns the leading entry, or `none` if the leaderboard is empty.
///
/// Entries are kept ranked highest-first, so the leader is simply the first.
#[view]
public fun highest_score(board: &Leaderboard): Option<ScoreEntry> {
if (board.entries.is_empty()) option::none() else option::some(board.entries[0])
}
The complete auction module:
// Copyright (c) 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
module view_functions::auction;
use iota::clock::Clock;
use iota::dynamic_field;
use std::string::String;
#[error(code = 0)]
const EAuctionEnded: vector<u8> = b"The auction has already ended.";
#[error(code = 1)]
const EBidTooLow: vector<u8> = b"The bid does not exceed the current highest bid.";
/// A shared English auction for a single lot.
public struct Auction has key {
id: UID,
/// Human-readable description of the lot on sale.
lot: String,
seller: address,
/// Unix timestamp (ms) after which no further bids are accepted.
ends_at: u64,
/// The highest amount bid so far; zero until the first bid.
highest_bid: u64,
/// Every bid placed, in the order received.
bids: vector<Bid>,
}
/// A single bid placed on the auction.
public struct Bid has copy, drop, store {
bidder: address,
/// Bid amount in the smallest currency unit.
amount: u64,
}
/// Dynamic-field key for a bidder's running tally.
public struct BidderKey has copy, drop, store {
bidder: address,
}
/// A bidder's activity, stored as a dynamic field keyed by their address.
public struct BidderTally has store {
/// How many bids this bidder has placed.
count: u64,
/// This bidder's highest bid so far.
highest: u64,
}
/// Create and share a new auction for `lot`, open until `ends_at`.
public fun create(lot: String, ends_at: u64, ctx: &mut TxContext) {
transfer::share_object(Auction {
id: object::new(ctx),
lot,
seller: ctx.sender(),
ends_at,
highest_bid: 0,
bids: vector[],
});
}
/// Place a bid on the auction.
///
/// Aborts if the auction has ended or if `amount` does not beat the current
/// highest bid. This mutates the auction, so it is a regular function, not a
/// view.
public fun place_bid(auction: &mut Auction, amount: u64, clock: &Clock, ctx: &TxContext) {
assert!(clock.timestamp_ms() < auction.ends_at, EAuctionEnded);
assert!(amount > auction.highest_bid, EBidTooLow);
let bidder = ctx.sender();
auction.highest_bid = amount;
auction.bids.push_back(Bid { bidder, amount });
let key = BidderKey { bidder };
if (dynamic_field::exists_(&auction.id, key)) {
let tally: &mut BidderTally = dynamic_field::borrow_mut(&mut auction.id, key);
tally.count = tally.count + 1;
tally.highest = amount;
} else {
dynamic_field::add(&mut auction.id, key, BidderTally { count: 1, highest: amount });
}
}
/// Returns an immutable reference to the whole list of bids.
///
/// A view may return an immutable reference into an object it received by
/// reference; only mutable references are disallowed.
#[view]
public fun bids(auction: &Auction): &vector<Bid> {
&auction.bids
}
/// Returns an immutable reference to the bid at `index`.
#[view]
public fun bid_at(auction: &Auction, index: u64): &Bid {
&auction.bids[index]
}
/// Returns an immutable reference to a bidder's tally, stored as a dynamic field.
#[view]
public fun tally_of(auction: &Auction, bidder: address): &BidderTally {
dynamic_field::borrow(&auction.id, BidderKey { bidder })
}
/// Returns the bid's amount together with a reference to the bid.
///
/// A view may return a tuple, and that tuple may contain immutable references.
#[view]
public fun bid_with_amount(auction: &Auction, index: u64): (u64, &Bid) {
let bid = &auction.bids[index];
(bid.amount, bid)
}
/// Reads the amount held by a bid through an immutable reference.
#[view]
public fun amount(bid: &Bid): u64 {
bid.amount
}
The complete vault module:
// Copyright (c) 2026 IOTA Stiftung
// SPDX-License-Identifier: Apache-2.0
module view_functions::vault;
use iota::clock::Clock;
#[error(code = 0)]
const EStillLocked: vector<u8> = b"The vault is still time-locked.";
#[error(code = 1)]
const ENotAllowed: vector<u8> = b"Sender is not allowed to unlock this vault.";
/// A shared, generic vault holding a single value of type `T`.
///
/// The contents stay locked until `unlock_at`, after which only `beneficiary`
/// may release them.
public struct Vault<T: store> has key {
id: UID,
item: T,
/// Unix timestamp (ms) before which the vault cannot be unlocked.
unlock_at: u64,
/// The only address permitted to unlock the vault.
beneficiary: address,
}
/// Create and share a vault wrapping `item`, locked until `unlock_at` and
/// releasable only by `beneficiary`.
public fun create<T: store>(item: T, unlock_at: u64, beneficiary: address, ctx: &mut TxContext) {
transfer::share_object(Vault { id: object::new(ctx), item, unlock_at, beneficiary });
}
/// Unlock the vault and return the stored item.
///
/// Aborts if the time lock has not elapsed, or if the sender is not the
/// `beneficiary`.
public fun unlock<T: store>(vault: Vault<T>, clock: &Clock, ctx: &TxContext): T {
assert!(clock.timestamp_ms() >= vault.unlock_at, EStillLocked);
assert!(ctx.sender() == vault.beneficiary, ENotAllowed);
let Vault { id, item, unlock_at: _, beneficiary: _ } = vault;
id.delete();
item
}
/// Returns an immutable reference to the stored item.
///
/// The view itself places no constraint on `T`: a type parameter used only
/// behind a reference may be unconstrained.
#[view]
public fun item<T: store>(vault: &Vault<T>): &T {
&vault.item
}
/// Returns the timestamp (ms) before which the vault stays locked.
#[view]
public fun unlock_at<T: store>(vault: &Vault<T>): u64 {
vault.unlock_at
}
/// Returns the only address allowed to unlock the vault.
#[view]
public fun beneficiary<T: store>(vault: &Vault<T>): address {
vault.beneficiary
}
/// Returns true if the time lock has elapsed as of `clock`.
#[view]
public fun is_unlockable<T: store>(vault: &Vault<T>, clock: &Clock): bool {
clock.timestamp_ms() >= vault.unlock_at
}