简体   繁体   中英

How to check if a NEAR account has a smart-contract deployed and implements a required interface?

Is there a way to check inside the smart contract (in Rust) if another account has a smart-contract associated with it and implements some interface?

In particular, in this function I would like to check if a recipient is a smart contract and if it has a required method:

trait FTReceiver {
    fn on_nft_receive(&self, sender: AddressID, token: AddressID) -> bool;
}
pub fn transfer(&mut self, recipient: AccountID, reference: String) {
    // note: env::* functions below don't exist!
    if env::has_smart_contract(recipient) && env::ctr_implements(recipient, FTReceiver) {
        Promise::new(token.clone()).function_call(
            "on_ft_receive".into(),
            &serde_json::to_vec(&json!({
                "sender": env::predecessor_account_id() /*...*/
            })),
        );
    }
}

The only way to check that is try & fail approach:

  1. Make an optimistic call
  2. Design enough callbacks / promises to handle fail situations.

Unfortunately this is complex and we can't handle the edge cases. We can't check in a reliable way why a Promise failed. The promise can fail because of: A) an account doesn't have a smart-contract, B) smart-contract doesn't have a called function C) function failure (eg assertion fail). This limitation is described in Simulation Tests repository: Error messages early in promise execution . Simulation tests are using exactly same runtime code as the blockchain.

Workaround

As @vlad-frolow noticed, we smart contracts can use view methods to report their interface. At the time of writing there is no standard for this view methods.

One idea:

pub fn implements_nep(&self, nep String) -> bool {
   nep == "21" || nep == ...

}

Note:

There is an ongoing work to inspect external accounts in the NEAR runtime.

Here is a very sample crude implemetation:

Receiver interface declaration:

use near_sdk::{ AccountId, Balance, ext_contract };
use near_sdk::json_types::U128;

/* The smart contract interface for handing incoming token transfers of Advanced Fungible.
 *
 */
#[ext_contract(ext_token_receiver)]
pub trait Receiver {

    /// Interface check promise to check if the receiver contract is able to handle Advanced Fungible
    /// Always return true
    fn is_receiver(self) -> PromiseOrValue<bool>;

    /// Notified after the balance transfer is complete. Must return true to finalise the transaction.
    /// TODO: More advanced error code / return value needed
    fn on_token_received(&mut self, sender_id: AccountId, amount_received: U128, amount_total: U128, message: Vec<u8>) -> PromiseOrValue<bool>;
}

Then checking if the interface is implemented:

        let promise0 = env::promise_create(
            new_owner_id.clone(),
            b"is_receiver",
            &[],
            0,
            SINGLE_CALL_GAS/3,
        );

        let promise1 = env::promise_then(
            promise0,
            env::current_account_id(),
            b"handle_receiver",
            json!({
                "old_owner_id": owner_id,
                "new_owner_id": new_owner_id,
                "amount_received": amount.to_string(),
                "amount_total": new_target_balance.to_string(),
                "message": message,
            }).to_string().as_bytes(),
            0,
            SINGLE_CALL_GAS/3,
        );

        env::promise_return(promise1);

And then taking action based on the call result:

    /**
     * After trying to call receiving smart contract if it reports it can receive tokens.
     *
     * We gpt the interface test promise back. If the account was not smart contract, finalise the transaction.
     * Otherwise trigger the smart contract notifier.
     */
    pub fn handle_receiver(&mut self, old_owner_id: AccountId, new_owner_id: AccountId, amount_received: U128, amount_total: U128, message: Vec<u8>) {
        // Only callable by self
        assert_eq!(env::current_account_id(), env::predecessor_account_id());
        env::log(b"handle_receiver reached");

        let uint_amount_received: u128 = amount_received.into();
        let uint_amount_total: u128 = amount_total.into();

        if is_promise_success() {
            // The send() was destined to a compatible receiver smart contract.
            // Build another promise that notifies the smart contract
            // that is has received new tokens.
        } else {
            // Non-code account or incompatible smart contract
            // Finalise transaction now.
            self.ledger.finalise(new_owner_id, uint_amount_received);
        }
    }


Full code: https://github.com/miohtama/advanced-fungible/blob/master/contract/token/src/receiver.rs

Optimally, NEAR should offer a branching primitive in in promise chains, so that the conditional chains could be build right off the start and you would have one less round trip back to your original caller contract after the interface check.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM