Owner's Callbacks

Here we put down the code for callbacks. Unless necessary, we won't discuss them in depth. As usual callbacks, you have necessary assertions. These callbacks, unlike the ones in foundation_callbacks.rs, don't return Promises. Instead, most of these return bool and most are checks on whether something is true or not before returning with the boolean to the main function in owner.rs.

In fact, we would skip some of these functions not necessary for discussion here. You can refer to the actual ones and fill it in, via the link in the references below.

As usual, they have a #[callback] argument passed in to replace the return value from near_sdk::env as we stated previously.

Note that some of the functions doesn't have a false return. It only returns true. Alternatively, it can panic if it doesn't meet any of the assertions.

If you check on how to optimize for wasm contract (link to LNC website when it became available), reducing the use of format! and prefer &str helps. However, when one tries to take it out from env::log_str into its own function like this,

impl LockupContract {
    fn logging_string(string: &str, amount: u128) -> &str {
      format!(
        string,
        amount.0,
        self.staking_information
            .as_ref()
            .unwrap()
            .staking_pool_account_id
            .clone()
      ).as_str()
    }

    ...
}

It has errors:

error: format argument must be a string literal
  --> src/owner_callbacks.rs:12:9
   |
12 |         string,
   |         ^^^^^^
   |
help: you might be missing a string literal to format with
   |
12 |         "{} {} {}", string,
   |         +++++++++++

So we can't actually take it out, and alternatively, you could just create a function that is read-only that returns the staking_pool_account_id; one isn't sure if the result returned will be the same (theoretically one hypothesizes it will). You can try, for sure, with some in-code assertions to check before deployment.

If you really need, certainly you can use a const for &str with this crate and use either concatcp! to replace concat! or formatcp! to replace format!.

We also decide to have this function taken out into internal.rs so we don't need to repeat it everytime.

use near_sdk::require;
use crate::*;

/*********************
 * Internal Methods
 *********************/

impl LockupContract {
    /// Balance excluding storage staking balance. 
    /// Storage staking balance can't be transferred out
    /// without deleting this contract.
    pub(crate) fn get_account_balance(&self) -> WrappedBalance {
      env::account_balance()
          .saturating_sub(MIN_BALANCE_FOR_STORAGE)
          .into()
    }

    pub(crate) fn set_staking_pool_status(
      &mut self,
      status: TransactionStatus
    ) {
      self.staking_information
          .as_mut()
          .expect("Staking pool should be selected.")
          .status = status;
    }

    pub(crate) fn set_termination_status(
      &mut self,
      status: TerminationStatus
    ) {
      if let VestingInformation::Terminating(termination_information) = 
          &mut self.vesting_information 
      {
        termination_information.status = status;
      } else {
        unreachable!("Vesting information not at terminating stage. ");
      }
    }

    pub(crate) fn assert_vesting(
      &self,
      vesting_schedule_with_salt: Option<VestingScheduleWithSalt>,
    ) -> VestingSchedule {
      match &self.vesting_information {

        VestingInformation::VestingHash(hash) => {
          if let Some(vesting_schedule_with_salt) = vesting_schedule_with_salt {
            require!(
              &vesting_schedule_with_salt.hash() == &hash.0,
              "Presented vesting schedule and salt don't match the hash."
            );
            vesting_schedule_with_salt.vesting_schedule
          } else {
            env::panic_str("Expected vesting schedule and salt, but not provided.")
          }
        }

        VestingInformation::VestingSchedule(vesting_schedule) => {
          require!(
            vesting_schedule_with_salt.is_none(),
            "Explicit vesting schedule already exists."
          );
          vesting_schedule.clone()
        }

        VestingInformation::Terminating(_) => env::panic_str("Vesting was terminated."),

        VestingInformation::None => env::panic_str("Vesting is None."),
      }
    }

    pub(crate) fn assert_staking_pool_is_idle(&self) {
      require!(
        self.staking_information.is_some(),
        "Staking pool is not selected."
      );

      match self.staking_information.as_ref().unwrap().status {
        
        TransactionStatus::Idle => (),

        TransactionStatus::Busy => {
          env::panic_str("Contract currently busy with another operation. ")
        }
      };
    }

    pub(crate) fn assert_staking_pool_is_not_selected(&self) {
      require!(
        self.staking_information.is_none(),
        "Staking pool is already selected"
      );
    }

    pub(crate) fn assert_no_termination(&self) {
      if let VestingInformation::Terminating(_) = &self.vesting_information {
        env::panic_str(
          "All operations are blocked until vesting termination is completed." 
        );
      }
    }



    pub(crate) fn assert_called_by_foundation(&self) {
      if let Some(foundation_account_id) = &self.foundation_account_id {
        require!(
          &env::predecessor_account_id() == foundation_account_id,
          "Can only be called by NEAR Foundation"
        )
      } else {
        env::panic_str("No NEAR Foundation account specified in the contract.");
      }
    }


    pub(crate) fn assert_owner(&self) {
      require!(
        &env::predecessor_account_id() == &self.owner_account_id,
        "This method can only be called by the owner. "
      )
    }

    pub(crate) fn assert_transfers_enabled(&self) {
      require!(
        self.are_transfers_enabled(),
        "Transfers are disabled."
      );
    }

    pub(crate) fn assert_transfers_disabled(&self) {
      require!(
        !self.are_transfers_enabled(),
        "Transfers are already enabled."
      );
    }


    pub(crate) fn assert_no_staking_or_idle(&self) {
      if let Some(staking_information) = &self.staking_information {
        match staking_information.status {
          TransactionStatus::Idle => (),
          TransactionStatus::Busy => {
            env::panic_str("Contract is currently busy with another operation.")
          }
        };
      }
    }


    pub(crate) fn staking_pool_account_id_clone(&self) -> AccountId {
      self.staking_information
          .as_ref()
          .unwrap()
          .staking_pool_account_id
          .clone()
    }
}

And we can call it via self.staking_pool_account_id_clone() when required, inside LockupContract.

Unable to not use unwrap

To make the contract lightweight, we prefer to not use unwrap nor expect because it'll cause panic!, which isn't ideal. Instead, prefer to use unwrap_or or unwrap_or_else or other methods like match instead.

However, there is one situation when this couldn't really be changed: when it's on the LHS (left-hand side) of the equation. Ultimately, unwrap_or will assign a value to the LHS if it's an equation/algorithm of the RHS. But if you are to assign to it, then one cannot think of another way to go around it. For example, the last line of this function:

use crate::*;
use near_sdk::{
  near_bindgen, PromiseOrValue, assert_self, 
  is_promise_success, require 
};


#[near_bindgen]
impl LockupContract {

    pub fn on_whitelist_is_whitelisted(
      &mut self,
      #[callback] is_whitelisted: bool,
      staking_pool_account_id: AccountId
    ) -> bool {
      assert_self();
      require!(
        is_whitelisted,
        "The given staking pool ID is not whitelisted."
      );
      self.assert_staking_pool_is_not_selected();
      self.assert_no_termination();

      self.staking_information = Some(StakingInformation {
        staking_pool_account_id,
        status: TransactionStatus::Idle,
        deposit_amount: 0.into(),
      });

      true
    }

    /// Deposit amount transferred from this account to staking pool.
    /// Required to update staking pool status.
    pub fn on_staking_pool_deposit(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let deposit_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if deposit_succeeded {
        self.staking_information
            .as_mut()
            .unwrap()
            .deposit_amount
            .0 
          += amount.0;
        
        env::log_str(
           format!(
             "Depositing {} to @{} succeeded.",
             amount.0,
             self.staking_pool_account_id_clone(),
           ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Depositing {} to @{} failed.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }

      deposit_succeeded
    }

    /// Deposit out to staking pool and staked. To update the
    /// staking pool status.
    pub fn on_staking_pool_deposit_and_stake(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let deposit_and_stake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if deposit_and_stake_succeeded {
        self.staking_information
            .as_mut()
            .unwrap()
            .deposit_amount
            .0
          += amount.0;

        env::log_str(
          format!(
            "Depositing and staking {} to @{} succeeded.",
            amount.0,
            self.staking_pool_account_id_clone(),
            ).as_str(),
         );
 
       } else {
         env::log_str(
           format!(
             "Depositing  and staking{} to @{} failed.",
             amount.0,
             self.staking_pool_account_id_clone(),
           ).as_str(),
         );
      }

      deposit_and_stake_succeeded
    }

    /// Requested given amount transferring from staking pool
    /// to this account. Update staking pool status.
    pub fn on_staking_pool_withdraw(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let withdraw_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if withdraw_succeeded {
        {
          let staking_information = self.staking_information
                                        .as_mut()
                                        .unwrap();
          // Deposit can be negative due to rewards
          staking_information.deposit_amount.0 = staking_information
              .deposit_amount
              .0
              .saturating_sub(amount.0);
        }

        env::log_str(
          format!(
            "Withdrawing {} from @{} succeeded",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Withdrawing {} from @{} failed",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }

      withdraw_succeeded
    }

    /// Called after extra amount stake was staked. 
    pub fn on_staking_pool_stake(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let stake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if stake_succeeded {
        env::log_str(
          format!(
            "Staking {} at @{} succeeded.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Staking {} at @{} failed.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }

      stake_succeeded
    }

    /// Called after given amount unstaked at staking pool.
    pub fn on_staking_pool_unstake(
      &mut self,
      amount: WrappedBalance
    ) -> bool {
      assert_self();

      let unstake_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_succeeded {
        env::log_str(
          format!(
            "Unstaking {} at @{} succeeded.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Unstaking {} at @{} failed.",
            amount.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }
      unstake_succeeded
    }

    /// All tokens unstaked from staking pool.
    pub fn on_staking_pool_unstake_all(&mut self) -> bool {
      assert_self();

      let unstake_all_succeeded = is_promise_success();
      self.set_staking_pool_status(TransactionStatus::Idle);

      if unstake_all_succeeded {
        env::log_str(
          format!(
            "Unstake all at @{} succeeded.",
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

      } else {
        env::log_str(
          format!(
            "Unstake all at @{} failed.",
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );
      }

      unstake_all_succeeded
    }

    /// Called after the transfer voting contract was checked for the vote result.
    pub fn on_get_result_from_transfer_poll(
      &mut self,
      #[callback] poll_result: PollResult,
    ) -> bool {
      assert_self();
      self.assert_transfers_disabled();

      if let Some(transfers_timestamp) = poll_result {
        env::log_str(
          format!(
            "Transfers were successfully enabled at {}",
            transfers_timestamp.0
          ).as_str(),
        );

        self.lockup_information
            .transfers_information = TransfersInformation::TransfersEnabled {
              transfers_timestamp,
            };

        true

      } else {
        env::log_str("Transfers not yet enabled.");
        false
      }
    }


    /// Called after the request to get the current total balance from the 
    /// staking pool.
    pub fn on_get_account_total_balance(
      &mut self,
      #[callback] total_balance: WrappedBalance
    ) {
      assert_self();
      self.set_staking_pool_status(TransactionStatus::Idle);

      env::log_str(
        format!(
          "Staking pool current total balance: {}",
          total_balance.0
        ).as_str(),
      );

      self.staking_information.as_mut().unwrap().deposit_amount = total_balance;
    }

    /// Called after the request to get the current unstaked balance to withdraw
    /// everything by the owner. 
    pub fn on_get_account_unstaked_balance_to_withdraw_by_owner(
      &mut self,
      #[callback] unstaked_balance: WrappedBalance
    ) -> PromiseOrValue<bool> {
      assert_self();

      if unstaked_balance.0 > 0 {
        // Withdraw
        env::log_str(
          format!(
            "Withdrawing {} from @{}",
            unstaked_balance.0,
            self.staking_pool_account_id_clone(),
          ).as_str(),
        );

        ext_staking_pool::withdraw(
          unstaked_balance,
          self.staking_pool_account_id_clone(),
          NO_DEPOSIT,
          gas::staking_pool::WITHDRAW,
        ).then(
          ext_self_owner::on_staking_pool_withdraw(
            unstaked_balance,
            env::current_account_id(),
            NO_DEPOSIT,
            gas::owner_callbacks::ON_STAKING_POOL_WITHDRAW,
          )
        ).into()


      } else {
        env::log_str("No unstaked balance to withdraw from staking pool.");
        self.set_staking_pool_status(TransactionStatus::Idle);
        PromiseOrValue::Value(true)
      }
    }
}

Finally

Before we end, one put all the available unit test from the lib.rs of the original repo in. There will be lots of errors, particularly concerning AccountId and how to parse it. One already teaches you about this so you can certainly work it out yourself.

Another problem is the "panic message but expected substring" failure. As one don't agree with some of the panic substring (one prefer shorter English without wasting words), some of the substring don't match. We just need to change and replace the expected substring with what we entered in, in the test, not from the code.

Third, one of the panic message is long, as it panics from the contract. This is something that can't change, because when it panics, it tries to unwrap and have this message for Result or Option; so we just have to replace the long messages in. This of course may be unstable if the panic messages changes in the future again. You just need to figure out whether the panic message is still for the same reason by reading the panic_msg and update it to the newest panic string everytime the panic changes.

Also, when you do the cargo test, certainly there will be warnings about unused imports: you can remove them now, don't leave them in.

Next, we shall look at the final element of this chapter: Simulation testing.

References