Search code examples
rustnearprotocol

Staking-pool core contract - ext_contract macro and callbacks


  1. In the internal_restake function, why do we have to access the on_stake_action function as if it's an external cross contract call when its a function thats a part of the calling contract? Couldn't we just do .then(self.on_stake_action()) instead? I'm assuming it has something to do with the face that it's a callback from stake() promise call.

  2. In what situation would you make an interface for the contract itself like ext_self?

  3. What does the #[ext_contract()] macro do in a nutshell?


    -------- Staking Pool Contract Code Below -------------------- lib.rs lines 155~162

/// Interface for the contract itself.
#[ext_contract(ext_self)]
pub trait SelfContract {
    /// A callback to check the result of the staking action.
    fn on_stake_action(&mut self);
}

lib.rs lines 399~421

   pub fn on_stake_action(&mut self) {
        assert_eq!(
            env::current_account_id(),
            env::predecessor_account_id(),
            "Can be called only as a callback"
        );

        assert_eq!(
            env::promise_results_count(),
            1,
            "Contract expected a result on the callback"
        );
        let stake_action_succeeded = match env::promise_result(0) {
            PromiseResult::Successful(_) => true,
            _ => false,
        };

        // If the stake action failed and the current locked amount is positive, then the contract
        // has to unstake.
        if !stake_action_succeeded && env::account_locked_balance() > 0 {
            Promise::new(env::current_account_id()).stake(0, self.stake_public_key.clone());
        }
    }

internal.rs lines 8~22

    /// Restakes the current `total_staked_balance` again.
    pub(crate) fn internal_restake(&mut self) {
        if self.paused {
            return;
        }
        // Stakes with the staking public key. If the public key is invalid the entire function
        // call will be rolled back.
        Promise::new(env::current_account_id())
            .stake(self.total_staked_balance, self.stake_public_key.clone())
            .then(ext_self::on_stake_action(
                &env::current_account_id(),
                NO_DEPOSIT,
                ON_STAKE_ACTION_GAS,
            ));
    }

Solution

  • The reason for the "external" interface for the contract is that the call to the method is external since it is "called" as part of a promise action.

    /// Interface for the contract itself.
    #[ext_contract(ext_self)]
    pub trait SelfContract {
        /// A callback to check the result of the staking action.
        fn on_stake_action(&mut self);
    }
    

    Firstly in rust procedural macros a stream of tokens (pub, trait, SelfContract, ...) in the input and output. In this case the output is not a trait but rather a module](https://doc.rust-lang.org/reference/items/modules.html) named ext_self. Then a function on_stake_action is added to the module and modified with the self argument removed and three new arguments added and returns a Promise.

            Promise::new(env::current_account_id())
                .stake(self.total_staked_balance, self.stake_public_key.clone())
                .then(ext_self::on_stake_action(
                    &env::current_account_id(),
                    NO_DEPOSIT,
                    ON_STAKE_ACTION_GAS,
                ));
    

    Note ext_self is the module :: is the path separator to access on_stake_action and &env::current_account_id() is the receiver, NO_DEPOSIT is the attached deposit, and ON_STAKE_ACTION_GAS is the gas for the promise call. Furthermore the code for the implementation of the function is generated; it encodes the arguments to the function (in this case there aren't any) and creates a promise that calls the method on_stake_action.

    The reason the initial declaration for this is a trait is that it doesn't require an implementation and IDE's with good rust support will already expand this macro allowing you to use ext_self as a module even though you can't see that that is what it is.

    You raise a good point though that ext_contract macro treats calls to the same contract the same as others. So perhaps a new useful feature would be to create a new macro that will already use env::current_account_id().