Near Learn

Pledging NEAR

Accept NEAR pledges with a #[payable] method: track per-backer totals with tuple-keyed LookupMaps and charge each new backer for the storage they add.

Beginner9 min read3-question check

A campaign without backers is just a title. In this milestone backers can pledge NEAR by attaching it to a pledge call. The contract records how much each backer has given, keeps a running total per campaign, and makes the backer — not the contract — pay for the new records.

The pledge method#

msg.value ↔ env::attached_deposit()
Solidity
function pledge(uint256 id) external payable {
    Campaign storage c = campaigns[id];
    require(c.token == address(0), "token campaign");
    require(block.timestamp < c.deadline, "ended");

    if (pledges[id][msg.sender] == 0) {
        backers[id][c.backerCount++] = msg.sender;
    }
    pledges[id][msg.sender] += msg.value;
    c.raised += msg.value;
}
Rust
#[payable]
pub fn pledge(&mut self, campaign_id: CampaignId) {
    let backer = env::predecessor_account_id();
    let campaign = self.campaign(campaign_id);
    require!(campaign.token.is_none(), "this campaign takes a fungible token: use ft_transfer_call");
    require!(env::block_timestamp_ms() < campaign.deadline_ms, "campaign has ended");

    let storage_cost = self.ensure_backer(campaign_id, &backer);
    let attached = env::attached_deposit().as_yoctonear();
    require!(
        attached > storage_cost,
        format!("attach more than {storage_cost} yoctoNEAR (storage) to pledge")
    );
    self.add_pledge(campaign_id, &backer, attached - storage_cost);
}

pub fn get_pledge(&self, campaign_id: CampaignId, account_id: AccountId) -> U128 {
    U128(self.pledges.get(&(campaign_id, account_id)).copied().unwrap_or(0))
}
Coming from EVM

msg.sender ↔ env::predecessor_account_id(): the account that called this method directly. msg.value ↔ env::attached_deposit(), a NearToken (use .as_yoctonear() for the raw u128).

The Solidity version never thinks about storage cost. The NEAR version subtracts it from the deposit: the first pledge to a campaign pays a little for the new records; later pledges pay nothing extra.

Add both methods inside the #[near] impl Crowdfund block. Note the require! checks run before any write: if one fails, the call panics, the attached NEAR goes back to the caller and nothing is stored.

Internal helpers#

Add a second, plain impl block below the #[near] one — these methods are not callable from outside
Rust
// internal helpers: a plain impl block, so none of these are exported
impl Crowdfund {
    fn campaign(&self, id: CampaignId) -> &Campaign {
        self.campaigns
            .get(id)
            .unwrap_or_else(|| env::panic_str("no such campaign"))
    }

    fn campaign_mut(&mut self, id: CampaignId) -> &mut Campaign {
        self.campaigns
            .get_mut(id)
            .unwrap_or_else(|| env::panic_str("no such campaign"))
    }

    /// Creates the backer's records if they are new. Returns their storage cost in yoctoNEAR.
    fn ensure_backer(&mut self, campaign_id: CampaignId, backer: &AccountId) -> u128 {
        let key = (campaign_id, backer.clone());
        if self.pledges.contains_key(&key) {
            return 0;
        }
        let initial = env::storage_usage();

        self.pledges.insert(key, 0);
        let campaign = self.campaign_mut(campaign_id);
        let index = campaign.backer_count;
        campaign.backer_count += 1;
        self.backers.insert((campaign_id, index), backer.clone());

        self.pledges.flush();
        self.backers.flush();
        storage_cost_since(initial)
    }

    fn add_pledge(&mut self, campaign_id: CampaignId, backer: &AccountId, amount: u128) {
        let key = (campaign_id, backer.clone());
        let total = self.pledges.get(&key).copied().unwrap_or(0) + amount;
        self.pledges.insert(key, total);
        self.campaign_mut(campaign_id).raised += amount;
    }
}
  • `ensure_backer` is the only place that grows state per backer: one pledges entry and one backers slot. It measures exactly those bytes and returns their price. A returning backer costs 0.
  • `add_pledge` only changes values in place (u128 stays 16 bytes), so it never needs a storage charge. We will reuse it for token pledges in lesson 6, where there is no NEAR deposit at all.
  • Separate impl block. Only methods inside #[near] impl become contract entry points. Helpers in a plain impl are ordinary Rust functions — the NEAR equivalent of internal in Solidity.
Try it from the CLI (after deploying in lesson 10, or in the sandbox tests in lesson 9)
Shell
near contract call-function as-transaction crowdfund.you.testnet pledge \
  json-args '{"campaign_id": 0}' \
  prepaid-gas '30 Tgas' attached-deposit '2 NEAR' \
  sign-as alice.testnet network-config testnet sign-with-keychain send

near contract call-function as-read-only crowdfund.you.testnet get_pledge \
  json-args '{"campaign_id": 0, "account_id": "alice.testnet"}' \
  network-config testnet now
# a yoctoNEAR string slightly below "2000000000000000000000000":
# 2 NEAR minus the storage for alice's two new records

Check yourself

3 questions · progress saved in this browser

  1. 1.Which near-sdk call is the equivalent of Solidity’s msg.value?
  2. 2.A backer pledges to the same campaign twice. What does the second pledge pay for storage?
  3. 3.Why are ensure_backer and add_pledge defined in an impl block without #[near]?