Paginate views, bound every loop
Paginate NEAR view methods with from_index and limit, cap loop sizes, and never iterate an unbounded collection in a change method that can run out of gas.
Intermediate8 min read3-question check
Any loop whose length grows with the number of users will eventually hit a gas limit. It works in testing with ten entries and fails in production with ten thousand. In a view method that means the frontend can no longer load the list; in a change method it can mean a feature, or the funds it manages, is stuck for good.
The rule is simple: every loop has a bound the contract controls, not one that grows with state or that the caller picks freely.
Paginated views#
use near_sdk::store::IterableSet;
use near_sdk::{near, AccountId};
const DEFAULT_LIMIT: u32 = 50;
const MAX_LIMIT: u32 = 100;
#[near(contract_state)]
pub struct Registry {
members: IterableSet<AccountId>,
}
impl Default for Registry {
fn default() -> Self {
Self { members: IterableSet::new(b"m") }
}
}
#[near]
impl Registry {
pub fn get_members(&self, from_index: Option<u32>, limit: Option<u32>) -> Vec<AccountId> {
let from = from_index.unwrap_or(0);
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT);
self.members
.iter()
.skip(from as usize)
.take(limit as usize)
.cloned()
.collect()
}
pub fn members_count(&self) -> u32 {
self.members.len()
}
}Bounded work in change methods#
A method like "pay a reward to every holder" cannot be fixed with pagination on the client side, because a single call must finish within its gas. Two designs work. Pull, not push: record what each account is owed (or a global reward-per-share value) and let each user claim their own. Process in batches: keep a cursor in state and let each call handle at most N items, so anyone can call it again until the job is done.
use near_sdk::store::{LookupMap, Vector};
use near_sdk::{near, AccountId, PanicOnDefault};
const REWARD: u128 = 1_000;
const MAX_BATCH: u32 = 50;
#[near(contract_state)]
#[derive(PanicOnDefault)]
pub struct Rewards {
queue: Vector<AccountId>,
rewards: LookupMap<AccountId, u128>,
cursor: u32,
}
#[near]
impl Rewards {
/// Processes up to max_items queued accounts; returns how many were done.
pub fn process_batch(&mut self, max_items: u32) -> u32 {
let end = self
.cursor
.saturating_add(max_items.min(MAX_BATCH))
.min(self.queue.len());
for i in self.cursor..end {
if let Some(account) = self.queue.get(i) {
let current = self.rewards.get(account).copied().unwrap_or(0);
self.rewards.insert(account.clone(), current + REWARD);
}
}
let done = end - self.cursor;
self.cursor = end;
done
}
}Check yourself
3 questions · progress saved in this browser