Совершение межконтрактных звонков с NEAR

Когда вы начнете создавать сложную архитектуру смарт-контрактов на платформе NEAR, одной из самых важных вещей, которую вы захотите сделать, является создание модульной инфраструктуры, чтобы вы могли инкапсулировать повторно используемые бизнес-функции в одном месте, где их можно поддерживать, масштабировать, и самое главное — повторное использование.

В экосистемах программирования это может иметь множество названий, таких как модули, пакеты и т. д., но концепция всегда одна и та же — предоставить механизм для развертывания и повторного использования общих компонентов и создания приложений, которые управляют ими. Для криптовалютной среды компонентом будет смарт-контракт, и ваше децентрализованное приложение будет управлять этими компонентами. Однако сами смарт-контракты могут также нуждаться в повторном использовании этих повторно используемых компонентов — и решением этой проблемы в NEAR является кросс-контрактный вызов.

Примечание!

Этот пример написан на Rust и относится только к платформе NEAR. Это не курс программирования Rust или NEAR, и если вы видите незнакомые вам концепции или термины, обратитесь к Learn Rust (https://www.rust-lang.org/learn) или документации NEAR (https://www. .near-sdk.io/).

Мы собираемся определить два контракта — один, с которым будут взаимодействовать наши пользователи (DAPPContract), а второй — наш смарт-контракт (PettingService). В нашем примере пользователи будут заходить на веб-сайт PetStore Petting, где роботы будут гладить наших питомцев на основе этого взаимодействия.

#[derive(Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
#[serde(crate = "near_sdk::serde")]
pub struct Pet {
    happiness : u8,
    food: u8,
}

Это будет структура Pet, к которой мы будем обращаться и обновлять. В этом примере это простой контейнер данных, но он может иметь более сложную функциональность, поскольку это обычная структура. Обратите внимание, что здесь мы получаем Serialize и Deserialize. Нам это понадобится, так как Pet должен быть сериализуем с помощью serde, который мы определяем в следующей строке как определенный в крейте near_sdk::serde (всегда делайте это для вещей, которые вы сериализуете).

#[near_bindgen]
#[derive(BorshDeserialize, BorshSerialize, Default)]
pub struct Contract {
    pets: HashMap <u8, Pet>,
}
...
impl Contract {
    // pet a pet and return its happiness as a numeric value
    pub fn pet_a_pet(&self, pet_id:u8) -> Pet
    {
        let mut pet : Pet = pets.get(pet_id);
        pet.happiness += 10
        return pet;
    }
}

Здесь у нас есть наш Контракт, который выполняет работу по ласке питомца. Если мы вызовем этот смарт-контракт с соответствующим pet_id, нашего питомца будут гладить, что увеличит его счастье.

Итак, у нас есть один из наших строительных блоков. Если все, что мы когда-либо хотели сделать, это погладить домашнее животное, то это все, что нам нужно. Но давайте предположим, что у нас есть контракт, который вызывается нашим dApp, который получает некоторое значение, и только после этого нашего питомца гладят! Могут быть другие вещи, которые делает dApp, например, планирование рабочего процесса или некоторые другие функции, но, оставив функциональность pet_a_pet отделенной от остальной бизнес-логики — мы упрощаем этот подход, вводим масштаб для нашей команды разработчиков и разрешаем повторное использование для других контракты нами или другими людьми.

Итак, давайте рассмотрим вызывающую сторону этого контракта и посмотрим, что нужно сделать.

Чтобы сделать кросс-контрактный вызов, вам нужно определить некоторые вещи в вызывающей функции. Вы должны определить интерфейс того, что вы вызываете, и вы должны определить интерфейс обратного вызова, который получит результат вызова кросс-контракта. Почему обратный звонок? Потому что кросс-контрактный вызов не происходит немедленно (хотя есть оптимизации, которые помогают ему происходить на том же узле, что и вызывающий смарт-контракт). Когда вы выполняете кросс-контрактный вызов, некоторые квитанции управляются для обработки сообщений, передаваемых между блоками и т.п. Это выходит за рамки данного поста, но достаточно подробно описано здесь (https://docs.near.org/docs/tutorials/contracts/xcc-receipts).

С нашей точки зрения, как разработчиков смарт-контрактов, все, что нам нужно сделать, это определить «интерфейсы», которые являются трейтами в Rust, и извлечь результат из вызова кросс-контракта в нашей функции обратного вызова.

Шаг 1. Определите интерфейс (свойство) кросс-контрактной функции, которую вы хотите вызвать. В нашем случае мы хотим вызвать функцию pet_a_pet.

#[ext_contract(ext_petting_service)]
trait ExtPettingService {
    fn pet_a_pet(&self, pet_id:u8) -> Pet
}

Здесь мы использовали ext_contract (поставляется в NEAR SDK), который представляет собой модуль, содержащий все функции для межконтрактных вызовов.

use near_sdk::{ext_contract}; // used to handle remote contract invocation

Определить интерфейс «ext_petting_service». Это имя совершенно произвольно с нашей точки зрения, но вы должны выбрать что-то значимое, потому что вы будете ссылаться на него, когда будете вызывать кросс-контрактный вызов. Точно так же черту можно назвать как угодно. Внутри трейта мы определяем функцию pet_a_pet с подписью SAME SAME SAME, как и в контракте, который вы хотите вызвать.

Шаг 2. Определите интерфейс (черту) локального обратного вызова, который будет принимать результат межконтрактного вызова.

#[ext_contract(ext_self)]
trait ExtSelf {
    fn on_pet_a_pet_callback(&mut self) -> Pet;
}

Здесь мы определяем локальную функцию обратного вызова. Возможно, вы заметили, что между ними нет функциональной разницы в сигнатурах. Поведение определяется использованием, а не подписью.

Шаг 3. Определите службу вызова

Итак, теперь давайте посмотрим, как на самом деле сделать вызов из нашего контракта dApp. Помните, что это тот, на который изначально будут звонить пользователи.

pub const CALLBACK_GAS: Gas = Gas(5_000_000_000_000);
...
#[near_bindgen]
impl Contract {
    #[payable]
    pub fn buy_pet_happiness(&self, pet_id: u8, worker: String ) ->      
         Promise
    {
        let amount = env::attached_deposit();
        if amount > 0
        {
            ext_petting_service::pet_a_pet( 
                pet_id,
                AccountId::new_unchecked("foo.testnet".to_string(),
                0, //attach no yoctoNEAR to the method,
                CALLBACK_GAS
            )
            .then (
                ext_self::on_pet_a_pet_callback(
                    env::current_account_id(),
                    0,
                    CALLBACK_GAS
            ))
         }
    }    
}

Хорошо, я собираюсь пройтись по этому медленно, так как все здесь важно.

В первой строке мы просто определяем количество GAS, которое мы будем использовать для выполнения вызовов различных функций в кросс-контрактном вызове. Так как мы будем часто использовать это — оно сделано константой.

Далее мы определяем нашу функцию buy_pet_happiness в смарт-контракте. Это тот, который будет вызываться нашим интерфейсом dApp. Здесь важно то, что эта функция возвращает Promise — не базовый тип и не тот тип объекта, который вы в конечном итоге захотите вернуть вызывающей стороне.

Затем мы извлекаем значение, которое было передано контракту, так как это контракт с оплатой (обратите внимание, это просто для того, чтобы проиллюстрировать суть примера — ваша функция НЕ обязательно должна быть оплачиваемой).

Убедившись, что пользователь ничего не отправил, мы делаем кросс-контрактный вызов. Обратите внимание, что мы используем имя ext_petting_service, так как именно это мы определили конечную точку службы, как в свойстве

#[ext_contract(ext_petting_service)]

Вот почему я сказал, что вы хотите, чтобы это имя соответствовало тому, что оно на самом деле делает.

ext_petting_service::pet_a_pet( 
                pet_id,
                AccountId::new_unchecked("foo.testnet".to_string(),
                0, //attach no yoctoNEAR to the method,
                CALLBACK_GAS
            )

Так как мы передаем аргумент в pet_a_pet, мы помещаем этот аргумент здесь. Если бы у pet_a_pet было несколько аргументов, мы бы просто перечислили их один за другим. Например, предположим, что у вас есть интерфейс, который выглядит так

fn pet_a_pet(&self, pet_id:u8, song_to_sing: String ) -> Pet

В этом примере мы передаем второй параметр, определяющий песню, которую мы хотим петь нашему питомцу, когда его гладят. Этот кросс-контрактный вызов хотел бы

ext_petting_service::pet_a_pet( 
                pet_id, song_to_sing,
                AccountId::new_unchecked("foo.testnet".to_string(),
                0, //attach no yoctoNEAR to the method,
                CALLBACK_GAS
            )

Это возвращает обещание, и если вы хотите просто вызвать свою функцию и ничего не делать в результате, вы можете остановиться здесь.

Следующий аргумент — это AccountId, который определяет учетную запись, в которой находится контракт, который вы вызываете. Я определил «foo.testnet» как учетную запись, в которой развернут контракт, который мы бы вызвали (но это не так — не пытайтесь вызывать это). Если вы развернете свой контракт на «petme.robopetting.testnet», то это то, что вы должны указать здесь вместо «foo.testnet». Мы не присоединяем никакого yoctoNEAR к методу, потому что здесь это не нужно, и мы отправляем количество газа CALLBACK_GAS для выполнения нашего контракта. Обратите внимание, что если вы отправите больше, чем вам нужно — вам просто вернут то, что вы не использовали.

В нашем примере мы хотим вернуть нашего питомца после того, как его погладили, и вернуть его в dApp. Для этого мы определяем «тогда» обещания, которое определяет, что происходит, когда обещание завершается.

.then (
                ext_self::on_pet_a_pet_callback(
                    env::current_account_id(),
                    0,
                    CALLBACK_GAS
            ))

В этом случае мы говорим, что хотим вызвать другую функцию, определенную в ext_self. Мы знаем, что это локальный обратный вызов, потому что мы используем учетную запись env::current_account_id(). В противном случае это точно так же, как если бы мы вызывали контракт на другой конечной точке службы.

Это ОЧЕНЬ важный момент, и я хочу убедиться, что он не потеряется в примере. Когда вы определяете

#[ext_contract(blah)]

то, что вы действительно определяете, - это конечная точка, которую можно вызвать. Это AccountId, который вы используете при вызове контракта, независимо от того, является ли он чем-то «локальным» или в другой учетной записи. Опять же, если бы у вас были какие-то аргументы для передачи, вы бы сделали это здесь, как и в предыдущем вызове кросс-контракта.

Последним шагом является определение самого обратного вызова «on_pet_a_pet_callback».

fn on_pet_a_pet_callback(&mut self) -> Pet;

Мы ничего не передаем и хотим вернуть питомца. Это еще один, который мы будем принимать медленно

pub fn on_pet_a_pet_callback(&mut self) -> Pet
{
    let result = promise_result_as_success();
    if result.is_none()
    {
        env::panic_str("ERROR::We expected there to be a result from this call");
    }
    let pet: Pet = near_sdk::serde_json::from_slice::<Pet>(&result.unwrap()).expect("Unable to unwrap the result into a Pet");
    return Pet;
}

У этой функции есть много всего, поэтому давайте углубимся в нее. Прежде всего, в сигнатуре мы говорим, что эта функция обратного вызова возвращает домашнее животное. Но подождите, вы можете спросить — исходный вызов dApp возвращает Promise, а не Pet. Глубоко в недрах среды выполнения NEAR Pet отправляется обратно, прикрепленным к Promise, и его десериализованная версия JSON возвращается в dApp (или в командную строку, если вы вызываете это с помощью «near call»).

Это один из фундаментальных моментов, который вы должны понимать в отношении кросс-контрактного обратного вызова. Сам обратный вызов определяет тип, который отправляется обратно исходному вызывающему объекту.

Следующая строка не менее важна для понимания:

let result = promise_result_as_success();

promise_result_as_success определяется здесь (https://docs.rs/near-sdk/3.1.0/near_sdk/utils/fn.promise_result_as_success.html). Это возвращает параметр Rust из обещания, в котором мы находимся. Таким образом, мы гарантированно вернем либо структуру/объект/скаляр/и т. д. это результат вызова кросс-контракта, иначе мы получим None.

if result.is_none()
{
        env::panic_str("ERROR::We expected there to be a result from this call");
}

В этом следующем разделе мы проверяем, является ли результат None, и если да, мы паникуем контракт, чтобы он просто завершался с определенной ошибкой.

Далее мы подходим к самой важной и наименее объясненной строке во всей документации о вызовах между контрактами. Без понимания этой строки все, что мы хотим сделать, было бы пустой тратой времени.

let pet: Pet = near_sdk::serde_json::from_slice::<Pet>(&result.unwrap()).expect("Unable to unwrap the result into a Pet");

Помните, в самом начале я говорил, что наша структура Pet должна быть сериализуемой и десериализуемой? Вот почему. (Примечание: если вы возвращаете скаляры, векторы и т. д., вас это не беспокоит, но операция ТОЧНО) одинакова.

Мы говорим, что хотим завести питомца и хотим получить его из нашего результата.

let pet: Pet = near_sdk::serde_json::from_slice::<Pet>(&result.unwrap()).expect("Unable to unwrap the result into a Pet");

Поскольку мы начали с параметра, нам нужно развернуть параметр, чтобы мы могли получить сериализованные байты JSON, которые хранятся в результате.

let pet: Pet = near_sdk::serde_json::from_slice::<Pet>(&result.unwrap()).expect("Unable to unwrap the result into a Pet");

В этом разделе мы говорим, что хотим взять эти байты JSON и использовать serde_json для преобразования их в тип — в данном случае Pet. Если бы мы возвращали Vec‹u8›, эта строка выглядела бы так:

let pet_ids: Vec<u8> = near_sdk::serde_json::from_slice::<Vec<u8>>(&result.unwrap()).expect("Unable to unwrap the result into a Vector");

Точно так же, если бы мы просто возвращали u8:

let pet_id: u8 = near_sdk::serde_json::from_slice::<u8>(&result.unwrap()).expect("Unable to unwrap the result into a u8");

Итак, теперь мы вернули Питомца из перекрестного контракта. Питомца погладили, и его счастье увеличилось на 10. Теперь мы можем просто вернуть питомца, и он вернется к вызывающей командной строке или вызову JavaScript в виде JSON, который можно использовать и использовать для чего угодно.

Итак, теперь у вас есть все детали — все болезненные и трудные для сведения детали того, как выполнять межконтрактные вызовы с помощью NEAR в Rust. Все это было протестировано с SDK 4.x.

[dependencies]
near-sdk = "=4.0.0-pre.7"
serde = { version = "1.0", features = ["derive"] }

Я надеюсь, что это поможет кому-то еще понять, как все это устроено, чтобы люди могли выйти туда и написать лучшие смарт-контракты с протоколом NEAR. Если вы нашли опечатку или что-то подобное в этом посте, сообщите нам, и мы исправим ее. Мы также постараемся обновить эту страницу, если в будущем изменятся материалы для межконтрактных вызовов.

Желаем удачи — создайте что-нибудь УДИВИТЕЛЬНОЕ!