Deadline & claim
Enforce a campaign deadline with env::block_timestamp_ms() and let the creator claim the funds with a NEAR transfer Promise once the goal is reached.
Beginner5 min read3-question check
Campaigns already record a deadline_ms, and pledge refuses late pledges. Now the money has to go somewhere. In this milestone the creator claims everything raised once the deadline has passed and the goal was met.
It is the first time the contract sends NEAR out, which means it is the first time we create a Promise — and the first time we must think about the fact that a transfer on NEAR happens after our method returns.
Time on NEAR#
| EVM | NEAR (near-sdk) | |
|---|---|---|
| Current time | block.timestamp (seconds) | env::block_timestamp() (nanoseconds) or env::block_timestamp_ms() (milliseconds) |
| Type | uint256 | u64 |
| Who sets it | The block proposer | The block producer; blocks arrive roughly every second |
| Good for | Deadlines of minutes or more | Deadlines of minutes or more |
We store and compare milliseconds everywhere: deadline_ms = env::block_timestamp_ms() + duration_ms at creation, now < deadline_ms to pledge, now >= deadline_ms to claim or refund. Milliseconds also fit safely in a JavaScript number, which matters when the frontend reads them (lesson 8). As on any chain, do not build logic that depends on second-level precision.
The claim method#
function claim(uint256 id) external {
Campaign storage c = campaigns[id];
require(msg.sender == c.creator, "only creator");
require(block.timestamp >= c.deadline, "running");
require(c.raised >= c.goal, "goal not reached");
require(!c.claimed, "already claimed");
c.claimed = true; // effects
(bool ok, ) = c.creator.call{value: c.raised}("");
require(ok, "transfer failed"); // atomic: reverts everything
}pub fn claim(&mut self, campaign_id: CampaignId) -> Promise {
let now = env::block_timestamp_ms();
let campaign = self.campaign_mut(campaign_id);
require!(env::predecessor_account_id() == campaign.creator, "only the creator can claim");
require!(now >= campaign.deadline_ms, "campaign is still running");
require!(campaign.raised >= campaign.goal, "goal not reached");
require!(!campaign.claimed, "already claimed");
campaign.claimed = true; // effects before the interaction
Promise::new(campaign.creator.clone())
.transfer(NearToken::from_yoctonear(campaign.raised))
}Add claim to the #[near] impl block. The checks are identical; the difference is the last line. In Solidity the transfer happens inside the call and require(ok) can revert everything. On NEAR, Promise::new(..).transfer(..) only schedules a transfer receipt that executes after claim has finished and its state is saved.
- Returning the `Promise` makes the transaction’s final result depend on the transfer, so wallets and explorers show the claim as failed if the transfer fails. It also lets us chain a callback in the next lesson.
- `claimed = true` before the transfer. Between
claimand its transfer receipt, other transactions can run. Setting the flag first means a secondclaimin that window hits “already claimed”. This is checks-effects-interactions, and on NEAR it is not optional. - No 1 yoctoNEAR requirement. Sensitive methods often demand exactly 1 yoctoNEAR so they cannot be called with a function-call access key. Here the money can only ever go to the creator, so a key that triggers
claimcannot redirect it.
MoreDesign choice: why does only the creator claim?
The payout always goes to the creator, so you could let anyone trigger it (a keeper bot, a backer who wants the campaign settled). That is a fine design too — remove the predecessor check and the money still cannot go anywhere else. We keep the check so the creator controls when the payout happens, and so the course has one clear authorization example.
Check yourself
3 questions · progress saved in this browser