(Эта статья также доступна как учебник YouTube и является частью серии MLabs, консалтинговой компании Haskell и Rust, специализирующейся на разработке критически важного программного обеспечения и совместной работе между командами. Для будущих статей и других новостей MLabs, вы также можете подписаться на нас в Twitter @Mlabs10).

Введение

Для тех, кто не сталкивался, Якорь — это фреймворк для написания программ Солана (смарт-контрактов) на языке программирования Rust. Он разработан, чтобы уменьшить количество шаблонов, необходимых для написания кода, который может работать на блокчейне Solana, а также автоматически обрабатывать некоторые распространенные ловушки безопасности.

К сожалению, по самой своей природе иногда может быть немного сложно понять, что именно делает Anchor-код под капотом. Я определенно рекомендую новичкам в Solana писать код без Anchor, по крайней мере, для начала, чтобы лучше понять некоторые его дизайнерские решения и структуру.

На момент написания статьи также было не так много доступной документации, посвященной правильному модульному тестированию кода Anchor в Rust — все блоги, которые я нашел, посвящены тестированию в JavaScript. Этот блог здесь, чтобы исправить это!

Здесь я выложу все, что вам нужно знать, чтобы протестировать свои программы на Солане, не выходя из безопасности языка Rust.

Весь изложенный ниже код полностью доступен на GitHub, если вы хотите увидеть его во всей красе.

Программа Солана

Перво-наперво: нам понадобится идея для написания программы на Anchor, чтобы нам было что тестировать. У меня была идея, о которой я хотел написать, которая казалась идеально подходящей: онлайн-сервис электронной почты. В частности, это программа Solana, которая позволяет отправлять «электронные письма» всем, для кого у вас есть открытый ключ учетной записи.

Выполнение этого в Solana дает нам несколько преимуществ по сравнению с решением вне сети:

  • Сообщения не могут быть удалены: поэтому существует постоянная общедоступная запись о том, кто кому отправил электронное письмо.
  • Это надежный носитель: сообщения не могут быть изменены третьими лицами.

Так как же выглядит реализация? Что-то вроде этого:

Основная структура представляет собой связанный список сообщений, где каждое сообщение указывает на предыдущее сообщение (кроме последнего сообщения, которое ни на что не указывает). Под «указанием» я просто подразумеваю, что в нем будет храниться открытый ключ следующего сообщения. Затем каждый пользователь будет управлять почтовым ящиком, который указывает на самое последнее сообщение, поступившее в папку «Входящие».

Последняя важная часть заключается в том, что сама программа будет владеть почтовым ящиком и всеми сообщениями (используя Программно-производные адреса или КПК), чтобы мы могли контролировать все изменения в этих структурах. С КПК сложно разобраться, поэтому не беспокойтесь, если вам будет трудно их понять. Я знаю, что я сделал, когда я впервые столкнулся с ними!

Итак, вот краткое описание того, что нам нужно написать:

  • Две структуры данных учетной записи: одна для почтового ящика пользователя и одна для отдельного сообщения.
  • Метод поиска почтового ящика учетной записи Solana по их открытому ключу
  • Программа Solana с единственной инструкцией «отправить сообщение», которая создает новое сообщение (и новый почтовый ящик, если принимающий пользователь никогда не получал сообщения), а затем добавляет сообщение в свой почтовый ящик.

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

Якорная реализация

Вот часть, которую вы с нетерпением ждали; написание реального кода! Во-первых, нам понадобятся несколько файлов, в которые можно что-то вставить (я не буду усложнять и буду использовать только один файл, так как это не слишком много кода):

src/
  lib.rs
Cargo.toml

И нам нужно заполнить Cargo.toml несколькими зависимостями:

(Для зорких глаз: я объясню это features=["init-if-needed"] через мгновение).

Теперь давайте заполним этот lib.rs. Вверху нам нужен импорт из Anchor и Solana:

Затем мы начинаем со структур данных, которые нам понадобятся, с почтовым ящиком и сообщением:

#[account] — это макрос Anchor, который добавляет для нас полезный шаблонный код.

Нам также понадобится знать требования к пространству для этих структур позже: к сожалению, вам придется вычислять их вручную, но есть хорошая ссылка здесь, чтобы рассказать вам, как это работает.

Я устанавливаю размер сообщения всегда не более 255 байт в качестве произвольного ограничения, чтобы сделать код здесь немного проще.

Далее нам нужен список учетных записей, которые нам понадобятся для нашей инструкции «отправить сообщение»:

Это сложный блок кода, поэтому позвольте мне его разобрать. Высокий уровень, у нас есть пять учетных записей, которые нам нужны:

  1. from: учетная запись лица, отправляющего сообщение, это Signer, потому что они платят за транзакцию, поэтому она должна быть подписана ими для авторизации платежей.
  2. to: учетная запись человека, которому нужно отправить сообщение.
  3. mailbox: учетная запись, которая содержит данные структуры Mailbox (содержит адрес последнего сообщения в папке "Входящие").
  4. message: учетная запись, содержащая структуру Message (с текстом сообщения и адресом предыдущего сообщения в папке "Входящие").
  5. system_program: это учетная запись глобальной системной программы, которая нам нужна для инициализации учетных записей mailbox и message.

Здесь также нужно понять множество макросов Anchor, поэтому давайте рассмотрим их:

  1. #[derive(Accounts)]: добавляет весь шаблонный код для десериализации структуры при вызове инструкции
  2. #[instruction(message_seed: Vec<u8>)]: это позволяет нам получить доступ к полю из аргументов, переданных в инструкцию, в данном случае message_seed, что позволяет нам создать message КПК.
  3. #[account(...)]: это еще более сложный вопрос, и я рекомендую вам взглянуть на раздел Anchor Lang Book об ограничениях, если вы хотите понять его больше. Короче говоря, он создаст для нас КПК, если он еще не существует. Важно отметить, что начальным значением для адреса КПК mailbox является открытый ключ учетной записи to: поэтому очень легко найти адрес почтового ящика любого пользователя. Именно здесь появляется функция init_if_needed из предыдущего: так что мы можем инициализировать эту структуру только в первом сообщении и повторно использовать существующую для всех последующих сообщений.

И затем мы подходим к сути кода, фактической инструкции программы по отправке сообщения:

И снова макросы Anchor делают всю тяжелую работу здесь, на этот раз это #[program], который расширяет все до функций, необходимых для программы Solana. Затем весь код выполняет три действия: проверяет, что сообщение не слишком длинное, инициализирует все поля Message в message, а затем добавляет сообщение в конец связанного списка входящих.

Чтобы этот код скомпилировался, нам также понадобится структура ошибки:

Итак, мы почти закончили, осталось добавить еще несколько полезных функций. Вспомогательная функция для вычисления адреса mailbox КПК:

И вспомогательная функция для создания инструкции send_direct_message:

Здесь вы можете видеть, что мы заполняем vec! в том же порядке, что и поля в SendDirectMessage, макросы привязки десериализуют это для нас, когда инструкция фактически выполняется. Вы также можете заметить, что мы ссылаемся на instruction::SendDirectMessage: это определено для нас макросом #[program], который мы использовали ранее. И в последнюю очередь мы используем нашу функцию mailbox_pda для вычисления адреса учетной записи mailbox для нас.

И последнее, что нам нужно для работающей программы, это определение идентификатора программы:

Фу, сколько кода! Сделаем перерыв?

Расширение макросов с помощью cargo expand

Это немного в стороне, но я нашел команду cargo expand очень полезной при отладке некоторых проблем. В двух словах, он расширяет все макросы в файле, чтобы вы могли видеть фактический код Rust, которым он становится непосредственно перед его полной компиляцией. Особенно с такой системой, как Anchor, которая добавляет для вас много стандартного кода, это может быть очень удобно для отладки проблем.

Если вы хотите попробовать, вы можете установить его с помощью cargo install cargo-expand . Вам также необходимо использовать последнюю ночную версию (rustup install nightly, если у вас ее еще нет, вы можете запустить cargo +nightly expand).

Я не буду показывать весь вывод (так как неудивительно, что он довольно большой), но вот фрагмент, который показывает, во что расширяется макрос declare_id!:

Написание тестов

Теперь самое важное: написать несколько модульных тестов. Для тех, кто не знает, в rust есть отличная встроенная инфраструктура тестирования, которую я рекомендую вам изучить, если вы еще не использовали ее. Прежде всего, мы сделаем тестовую заглушку по адресу tests/integration.rs, где она будет автоматически обнаружена cargo:

Как вы могли заметить, здесь я использую ключевое слово async, чтобы сделать этот тест асинхронным, с отличным крейтом tokio в качестве асинхронного запуска. Если вы не знакомы с асинхронностью, то я рекомендую заглянуть в Книгу асинхронности, но это не должно сильно повлиять на ваше понимание остального кода: это будут несколько ошибочных await ключевых слов, разбросанных вокруг того, что вы можно смело игнорировать.

Вы также можете заметить, что мы также повторно импортируем tokio из solana_program_test, а не как отдельный ящик: это упрощает согласование номеров версий. Не забудьте добавить эти импорты в program/Cargo.toml:

Следующим шагом является настройка виртуального валидатора Solana в памяти, который может выполнять программу и загружать программу в него. Для этого мы будем использовать ящик solana-program-test:

Здесь следует отметить пару вещей: во-первых, ящик программы Solana (термин Solana для смарт-контракта) автоматически становится доступным здесь в пространстве имен program. Оттуда автоматически доступен уникальный идентификатор программы, который мы определили в вызове макроса declare_id!.

Также для нас автоматически определяется функция entry: она создается из атрибута макроса #[program], который мы использовали ранее. Отсюда начнется выполнение кода; что-то вроде эквивалента Solana fn main.

Затем нам нужно добавить несколько учетных записей для тестирования и дать им немного денег на карманные расходы для оплаты их транзакций (используя несколько импортов из ящика solana-sdk):

Затем мы можем запустить валидатор и начать тестирование (большая часть следующего кода находится в test_program, но я не буду повторяться с этого момента, просто поместив новые строки отдельно):

Первое, что нужно проверить, это то, что почтовые ящики еще не существуют, у нас должна быть хорошая чистая начальная позиция:

context.banks_client — это интерфейс, который мы можем использовать для запроса валидатора, чтобы получить такие вещи, как счета и балансы.

После этого мы можем отправить наше первое сообщение, но перед этим нам понадобятся некоторые вспомогательные функции:

Эта функция вызывает вспомогательную функцию, которую мы создали в [lib.rs](<http://lib.rs>) send_direct_message, чтобы создать необходимую нам инструкцию, а затем отправить ее контекстному клиенту как новую транзакцию (подписанную отправителем для авторизации платежа).

Здесь следует отметить, что мы используем случайное начальное число для КПК сообщений. Вы не можете использовать что-то вроде отправителя или тела сообщения в качестве семени, потому что пользователь может отправить одно и то же сообщение дважды!

Сделав это, мы можем начать отправлять сообщения в нашей тестовой функции:

(Я назвал сообщение ciphertext, потому что оно действительно должно быть зашифровано, но, как я упоминал выше, я не упомянул это для упрощения).

И теперь мы можем проверить, что сообщение сработало, как и ожидалось:

Мы используем клиент для получения данных учетных записей, а затем можем их десериализовать (с помощью функций, созданных для нас макросом #[account]), и проверить, что содержимое соответствует нашим ожиданиям.

И это все! Еще одно сообщение, чтобы проверить, все ли работает как положено:

И мы наконец закончили писать код. Слава Богу! Остается только одна задача: давайте прогоним эти тесты.

Запуск тестов

Обычно на этом этапе вы запускаете cargo test, но для правильной проверки нам нужно запустить cargo test-bpf (если вы впервые используете Solana в Rust, вам нужно установить набор инструментов Solana).

И это должно быть так! Надеюсь, вы получите что-то вроде этого:

test test_program ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.26s

Заключение

Если вы застряли со мной так далеко: спасибо! И, надеюсь, я смог показать вам, как написать небольшую программу Anchor Solana и, что более важно, как протестировать ее на Rust. Как я упоминал во вступлении, вы можете увидеть весь код, который я описал полностью, на GitHub, не стесняйтесь посетить его, чтобы получить лучшее представление о полной картине.

МЛабс

Новые технологии, такие как блокчейн, открывают большие перспективы, но создание безопасных и надежных продуктов — непростая задача. MLabs предоставляет необходимые услуги для проектов смарт-контрактов, в частности, в пространствах Cardano, Polkadot и Solana. Наша специализированная поддержка помогает некоторым клиентам преодолеть внутренние препятствия, в то время как другие полностью полагаются на MLabs для разработки.

Стартапам часто не хватает четких стратегий запуска. У других есть многообещающие идеи продукта, но они не могут быть реализованы. MLabs может заполнить данные там, где это необходимо, сократив время запуска и общие затраты на разработку. Благодаря комплексным решениям DevOps и индивидуальным предложениям наши услуги являются гибкими и комплексными. Консультационная компания по разработке программного обеспечения, специализирующаяся на Haskell и Rust, MLabs успешно помогла многочисленным командам, таким как ваша, разработать разнообразные продукты, многие из которых впервые вышли на рынок.

Свяжитесь с MLabs сегодня и запишитесь на бесплатную консультацию.