Search code examples
rustsmartcontractsnearprotocol

Elements of a nested collection in a smart contract won't persist


In NEAR testnet I have this Rust code of a smart contract which works oddly -- elements of a nested collection - Lottery.lottery_obj.users - won't persist between calls.


    pub struct Lottery {
        items: TreeMap<LotteryId, LotteryItem>,
        //....
    }

    pub struct LotteryItem {
        //......
        lottery_id: LotteryId, //GUID as string
        users: TreeMap<AccountId, User>,
    }

#[near_bindgen]
impl Lottery {

    //......
    #[init]
    pub fn init() -> Self {
        let items: TreeMap<LotteryId, LotteryItem> = TreeMap::new(b"t");
        Self {
          //.....
          items,
        }
    }

    pub fn add_lottery(&mut self, lottery_id: LotteryId) -> Option<LotteryId> {
        if !self.items.contains_key(&lottery_id) {
            let users: TreeMap<AccountId, Participant> = TreeMap::new(lottery_id.as_bytes());
            let new_item = LotteryItem { lottery_id, users };
            self.items.insert(&lottery_id.clone(), &new_item);
        } else {
            log!("lottery_id '{}' already exists; generate a new one", lottery_id);
            None
        }
    }

    //
    // 1
    //
    // lottery.users.len() == 0 always
    // unless I add an element and check its len() right away
    // next call it'll get 0 again
    //
    pub fn add_user(&self, lottery_id: LotteryId, user_account_id: AccountId) {
        let mut lottery = self.items.get(&lottery_id).unwrap();

        //always zero
        log!("users.len {}", lottery.users.len())

        if lottery.users.contains_key(&user_account_id) {
          log!("user {} already exists")
        } else {
            let new_user = User {
                name: "random_one",
            };

            lottery.users.insert(&user_account_id, &new_user);

            //BUT NOW IT'S 1
            log!("users.len {}", lottery.users.len())
        }
    }


    //
    //2
    //
    // always returns None
    pub fn get_user(&self, lottery_id: LotteryId, user_account_id: AccountId) -> Option<User> {
        let mut lottery = self.items.get(&lottery_id).unwrap();

        //always zero
        log!("users.len {}", lottery.users.len())

        lottery.users.get(&user_account_id)
    }
}

No matter what I do, the collection lottery_obj.users always remains empty between calls -- when I call the methods of a smart contract from the outside.

Each new method call lottery_obj.users.len() will be 0!

It only will get non empty within a call -- once I've added an element in it and then check its len().

The Lottery.items collection do work correctly, though - no elements will disappear.

What cases this? And how to fix it?


Solution

  • This question was answered on Discord (Links here and with code). For posterity, the answer was this:

    The parent collection was not updated when the nested collection was modified, so the metadata (including how many elements in the map) was not updated.

    This error pattern is documented https://docs.near.org/sdk/rust/contract-structure/nesting#error-prone-patterns as "Bug 2"

    Modified add_user method:

    pub fn add_user(&self, lottery_id: LotteryId, user_account_id: AccountId) {
        let mut lottery = self.items.get(&lottery_id).unwrap();
    
        //always 0
        log!("users.len {}", lottery.users.len())
    
        if lottery.users.contains_key(&user_account_id) {
          log!("user {} already exists")
        } else {
            let new_user = User {
                name: "random_one",
            };
    
            lottery.users.insert(&user_account_id, &new_user);
    
            //BUT NOW IT'S 1
            log!("users.len {}", lottery.users.len())
    
            //* FIX: need to insert the item back to update
            self.items.insert(&lottery_id, &lottery);
        }
    }