Views & pagination
Design frontend-friendly NEAR view methods: a computed campaign status, paginated campaign and backer lists, and U128 strings for large amounts in JSON.
Intermediate9 min read3-question check
The contract is functionally complete. What it lacks is a good read API: the frontend needs to list campaigns, show each one’s status, and list who backed it. This milestone adds those views — free for callers, but still bounded, because a view that tries to return everything eventually stops working.
A status the frontend doesn’t have to compute#
#[near(serializers = [json])]
#[derive(PartialEq, Debug)]
pub enum CampaignStatus {
Open,
Succeeded,
Failed,
}
// add the field to CampaignView
// pub status: CampaignStatus,
fn view_of(id: CampaignId, c: &Campaign) -> CampaignView {
let status = if env::block_timestamp_ms() < c.deadline_ms {
CampaignStatus::Open
} else if c.raised >= c.goal {
CampaignStatus::Succeeded
} else {
CampaignStatus::Failed
};
CampaignView {
id,
creator: c.creator.clone(),
title: c.title.clone(),
goal: U128(c.goal),
raised: U128(c.raised),
deadline_ms: c.deadline_ms,
token: c.token.clone(),
claimed: c.claimed,
backer_count: c.backer_count,
status,
}
}View calls run against a recent block, so env::block_timestamp_ms() works in them too. The status uses exactly the same comparisons as pledge, claim and refund, so the UI can never show “Failed” while the contract still accepts pledges. Unit variants serialize as strings: "Open", "Succeeded", "Failed".
Paginated lists#
function getCampaigns(uint256 from, uint256 limit)
external view returns (Campaign[] memory page)
{
uint256 end = from + limit;
if (end > campaigns.length) end = campaigns.length;
if (from >= end) return new Campaign[](0);
page = new Campaign[](end - from);
for (uint256 i = from; i < end; i++) {
page[i - from] = campaigns[i];
}
}const MAX_PAGE: u32 = 50;
pub fn get_campaigns(&self, from_index: Option<u32>, limit: Option<u32>) -> Vec<CampaignView> {
let from = from_index.unwrap_or(0);
let limit = limit.unwrap_or(20).min(MAX_PAGE);
let end = from.saturating_add(limit).min(self.campaigns.len());
(from..end)
.filter_map(|id| self.campaigns.get(id).map(|c| view_of(id, c)))
.collect()
}
pub fn get_backers(
&self,
campaign_id: CampaignId,
from_index: Option<u32>,
limit: Option<u32>,
) -> Vec<BackerView> {
let count = self.campaigns.get(campaign_id).map(|c| c.backer_count).unwrap_or(0);
let from = from_index.unwrap_or(0);
let limit = limit.unwrap_or(20).min(MAX_PAGE);
let end = from.saturating_add(limit).min(count);
(from..end)
.filter_map(|i| self.backers.get(&(campaign_id, i)))
.map(|account_id| BackerView {
account_id: account_id.clone(),
amount: self.get_pledge(campaign_id, account_id.clone()),
})
.collect()
}
pub fn get_campaign_count(&self) -> u32 {
self.campaigns.len()
}Option<u32> arguments can be omitted from the JSON: {} means “first page, 20 items”. The MAX_PAGE cap means a client cannot ask for a page so large the view runs out of gas.
A range of indexes reads only the entries on the page. This is why lesson 2 picked a Vector for campaigns and the (campaign_id, index) map for backers: both page by index, directly.
#[near(serializers = [json])]
pub struct BackerView {
pub account_id: AccountId,
pub amount: U128,
}Numbers in JSON#
| Value | Rust type | In JSON | Why |
|---|---|---|---|
| Goal, raised, pledges | u128 → U128 | "5000000000000000000000000" | JS numbers lose precision above 2^53; 5 NEAR is 5×10^24 yoctoNEAR |
| Deadline | u64 (milliseconds) | 1767225600000 | Milliseconds since 1970 stay far below 2^53 |
| Block time in nanoseconds | u64 → U64 | "1767225600000000000" | Nanoseconds exceed 2^53, so they need a string |
| Ids, counts | u32 | 7 | Always safe |
near contract call-function as-read-only crowdfund.you.testnet get_campaigns \
json-args '{"from_index": 0, "limit": 10}' network-config testnet now
near contract call-function as-read-only crowdfund.you.testnet get_backers \
json-args '{"campaign_id": 0}' network-config testnet nowCheck yourself
3 questions · progress saved in this browser