Lock unlock account

The first is how to lock and unlock an account. These are the definitions as usual.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

Note the decorator ext_contract(ext_self) means this is targeted for cross-contract calls. For more information, please refer to this page. Cross-contract calls are also known as "Callbacks".

Note since we're using near-sdk-rs v4 instead of v3, we shall also use some newest features like require! to replace assert!, assert_eq! and assert_ne!.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

The above function checks if a promise is successful. One actually doesn't know what a promise is before this (not a web developer anyways), but one learnt from the near-sdk rust doc it's something that won't be called immediately, but called on the future. What it means by future, according to the doc, is it's at least the next "block height" which it will execute, rather than at current "block height". If you don't know about how time is calculated, check out "Bitcoin is Time" article on the notable page.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

With init(ignore_state), this is an initialization method, where immediately when this started, this method will be called to first lock the account, regardless of what state it's currently in, for safety reason. Otherwise if it's in unlock state, anybody could access the account at initialization.

Only certain people can lock an account. In this case, predecessor means the previous person whom have ownership over the content, which should be the owner whom unlock it in the first place. Hence, if the current is the owner, (hence current == predecessor), we allow him/her to lock back his/her account. We don't allow a random person to come in and lock other people's account.

If you check the docs for AccountId, we found that the From implementation (under "Trait Implementation") is From<ValidAccountId>, so that means, calling AccountId.into() will convert the result to ValidAccountId type. This is just basic Rust, if you already know, great! If you don't know yet, hope you understand now.

In the same way, we see owner_id here is of type ValidAccountId but the definition requires owner_id to be AccountId, the .into() will convert it back to AccountId type.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

When we lock the account, we have a certain owner_id that we stored away. The owner_id is hypothesized to most probably be the current_account_id, otherwise it wouldn't pass the assertion. Here, we're retrieving the owner_id to compare that it matches, predecessor has not changed in some way, so initialize the unlocking.

Note: One isn't sure why it isn't compared to env::current_account_id() here though. Perhaps the contract can be hold by several person, each with different owner_id, so this might be checking that the correct contract is calling the correct predecessor? One don't know.

Originally they use AccountId::default(). Searching the docs for that particular version of near-sdk, one cannot find the impl Default trait for that. So, one read through the logic and assume that they're trying to set it back to None. However, one doesn't know how to do that. In fact, one think that since you are assigning it again when locking, perhaps we could let it be while unlocked. But what if someone hack and steal your AccountId while unlocked? One don't know.

Then we have a Promise to give a FullAccessKey to the owner of the account by the end.

use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
use near_sdk::json_types::{Base58PublicKey, ValidAccountId};
use near_sdk::{
  env, ext_contract, near_bindgen, AccountId, PanicOnDefault,
  Promise, PromiseResult, require, Gas
};

const ON_ACCESS_KEY_ADDED_CALLBACK_GAS: Gas = Gas(20_000_000_000_000);
const NO_DEPOSIT: u128 = 0;

near_sdk::setup_alloc!();

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)]
pub struct Contract {
    pub owner_id: AccountId,
}

#[ext_contract(ext_self)]
pub trait ExtContract {
    fn on_access_key_added(&mut self, owner_id: AccountId) -> bool;
}

fn is_promise_success() -> bool {
    require!(
      env::promise_results_count() == 1,
      "Contract expected a result on the callback"
    );

    match env::promise_result(0) {
      PromiseResult::Successful(_) => true,
      _ => false,
    }
}


#[near_bindgen]
impl Contract {
    #[init(ignore_state)]
    pub fn lock(owner_id: ValidAccountId) -> Self {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Current user is not allowed to init the contract"
      );

      Self { owner_id: owner_id.into() }
    }

    pub fn unlock(&mut self, public_key: Base58PublicKey) {
      require!(
        env::predecessor_account_id() == self.owner_id,
        "Current owner is not allowed to add a key"
      );

      // thinking of how to solve this... 
      // self.owner_id = AccountId("");

      Promise::new(env::current_account_id())
          .add_full_access_key(public_key.into())
          .then(ext_self::on_access_key_added(
            env::predecessor_account_id(),
            env::current_account_id(),
            NO_DEPOSIT,
            ON_ACCESS_KEY_ADDED_CALLBACK_GAS,
          ));
    }

    pub fn get_owner(&self) -> AccountId {
      self.owner_id.clone()
    }

    // Callback
    pub fn on_access_key_added(
      &mut self, 
      owner_id: AccountId
    ) -> bool {
      require!(
        env::predecessor_account_id() == env::current_account_id(),
        "Callback can only be called from the contract"
      );

      let access_key_created = is_promise_success();
      if !access_key_created {
        self.owner_id = owner_id  // put owner_id back if failed. 
      }
      access_key_created
    }
}

get_owner is just a view method, easy to understand. on_access_key_added is a callback, which is called from the Promise previously ext_self::on_access_key_added. Note that callbacks are usually called by the contract internally, hence usually it is fulfilled via a Promise. You don't want other people to call callbacks externally, as that can lead your contract to be vulnerable. We have to check that it's certainly called by "the" contract. This means the predecessor equals the current. The current is the one holding the contract, and he's also the previous owner of the contract (the contract never gets redeployed anywhere, eh, so you get the logic).

References