4. Наша первая инструкция

Перевод | Автор оригинала: Loris

ЭПИЗОД 4

2 МЕСЯЦА НАЗАД

13 МИН ЧТЕНИЕ

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

Определение контекста

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

Из-за этого отправка инструкции программе требует предоставления всего необходимого контекста для ее успешного выполнения.

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

В вашем файле lib.rs, прямо над структурой Tweet, которую мы определили в предыдущем эпизоде, вы должны увидеть пустой контекст Initialize.

#[derive(Accounts)]
pub struct Initialize {}

Давайте заменим этот контекст Initialize контекстом SendTweet и перечислим все нужные нам учетные записи.

Удалите две строки выше и замените их следующим кодом.

#[derive(Accounts)]
pub struct SendTweet<'info> {
    pub tweet: Account<'info, Tweet>,
    pub author: Signer<'info>,
    pub system_program: AccountInfo<'info>,
}

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

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

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

Хорошо, давайте быстро просмотрим перечисленные аккаунты:

Далее, давайте объясним некоторые особенности Rust, которые мы можем увидеть в приведенном выше коде.

Будьте уверены, нет необходимости углубляться в эти функции Rust, чтобы следовать этой серии. Я просто бросаю некоторые ссылки для заинтересованных читателей.

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

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

Ограничения аккаунта

Помимо помощи в определении контекстов инструкций всего в нескольких строках кода, Anchor также предоставляет нам ограничения учетной записи, которые можно определить как атрибуты Rust в свойствах нашей учетной записи.

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

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

#[derive(Accounts)]
pub struct SendTweet<'info> {
    #[account(init)]
    pub tweet: Account<'info, Tweet>,
    pub author: Signer<'info>,
    pub system_program: AccountInfo<'info>,
}

Однако приведенный выше код выдаст ошибку, потому что мы не сообщаем Anchor, сколько памяти требуется для нашей учетной записи Tweet и кто должен платить за освобождение от арендной платы. К счастью, для этой цели мы можем использовать аргументы payer и space.

#[derive(Accounts)]
pub struct SendTweet<'info> {
    #[account(init, payer = author, space = Tweet::LEN)]
    pub tweet: Account<'info, Tweet>,
    pub author: Signer<'info>,
    pub system_program: AccountInfo<'info>,
}

Аргумент payer ссылается на учетную запись author в том же контексте, а аргумент space использует константу Tweet::LEN, которую мы определили в предыдущем эпизоде. Разве не удивительно, что мы можем сделать все это всего одной строкой кода?

Теперь, поскольку мы говорим, что author должен платить за освобожденные от арендной платы деньги аккаунта tweet, нам нужно пометить свойство author как изменяемое. Это потому, что мы собираемся изменить сумму денег на их счету. Опять же, Anchor делает это очень простым для нас с ограничением учетной записи mut.

#[derive(Accounts)]
pub struct SendTweet<'info> {
    #[account(init, payer = author, space = Tweet::LEN)]
    pub tweet: Account<'info, Tweet>,
    #[account(mut)]
    pub author: Signer<'info>,
    pub system_program: AccountInfo<'info>,
}

Обратите внимание, что существует также ограничение учетной записи signer, которое мы могли бы использовать для свойства author, чтобы убедиться, что он подписал инструкцию, но в нашем случае это избыточно, потому что мы уже используем тип учетной записи Signer.

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

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

#[derive(Accounts)]
pub struct SendTweet<'info> {
    #[account(init, payer = author, space = Tweet::LEN)]
    pub tweet: Account<'info, Tweet>,
    #[account(mut)]
    pub author: Signer<'info>,
    #[account(address = system_program::ID)]
    pub system_program: AccountInfo<'info>,
}

system_program::ID — это константа, определенная в кодовой базе Solana. По умолчанию он не включен в импорт Anchor prelude::*, поэтому нам нужно добавить следующую строку позже – в самый верх нашего файла lib.rs.

use anchor_lang::prelude::*;
use anchor_lang::solana_program::system_program;

И точно так же мы закончили с определением контекста нашей инструкции SendTweet.

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

Реализация логики

Теперь, когда наш контекст готов, давайте реализуем реальную логику нашей инструкции SendTweet.

Внутри модуля solana_twitter замените функцию инициализации следующим кодом.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    Ok(())
}

Несколько замечаний:

Теперь, когда сигнатура нашей функции готова, давайте извлечем из контекста все учетные записи, которые нам понадобятся.

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

Мы можем получить доступ к учетной записи tweet через ctx.accounts.tweet. Поскольку мы используем Rust, нам также нужно добавить префикс &, чтобы получить доступ к учетной записи по ссылке, и mut, чтобы убедиться, что нам разрешено изменять ее данные.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;

    Ok(())
}

Точно так же нам нужно получить доступ к учетной записи author, чтобы сохранить его в учетной записи tweet. Здесь нам не нужен mut, потому что Anchor уже позаботился об освобождении от арендной платы.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;
    let author: &Signer = &ctx.accounts.author;

    Ok(())
}

Наконец, нам нужен доступ к системной переменной Solana Clock, чтобы определить текущую временную метку и сохранить ее в твите. Эта системная переменная доступна через Clock::get() и может работать только в том случае, если системная программа предоставляется в качестве учетной записи.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;
    let author: &Signer = &ctx.accounts.author;
    let clock: Clock = Clock::get().unwrap();

    Ok(())
}

Обратите внимание, что мы используем функцию unwrap(), потому что Clock::get() возвращает Result, который может быть Ok или Err. Развертка результата означает либо использование значения внутри Ok — в нашем случае часов — либо немедленное возвращение ошибки.

Включая topic и content, переданные в качестве аргументов, теперь у нас есть все данные, необходимые для заполнения нашей новой учетной записи tweet правильными данными.

Начнем с открытого ключа автора. Мы можем получить к нему доступ через author.key, но он содержит ссылку на открытый ключ, поэтому нам нужно разыменовать его, используя *.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;
    let author: &Signer = &ctx.accounts.author;
    let clock: Clock = Clock::get().unwrap();

    tweet.author = *author.key;

    Ok(())
}

Затем мы можем получить текущую метку времени UNIX из часов, используя clock.unix_timestamp.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;
    let author: &Signer = &ctx.accounts.author;
    let clock: Clock = Clock::get().unwrap();

    tweet.author = *author.key;
    tweet.timestamp = clock.unix_timestamp;

    Ok(())
}

Наконец, мы можем сохранить topic и content в их соответствующих свойствах.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;
    let author: &Signer = &ctx.accounts.author;
    let clock: Clock = Clock::get().unwrap();

    tweet.author = *author.key;
    tweet.timestamp = clock.unix_timestamp;
    tweet.topic = topic;
    tweet.content = content;

    Ok(())
}

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

Защита от неверных данных

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

В предыдущем эпизоде мы решили использовать тип String для свойств topic и content и выделить максимум 50 символов для первого и 280 символов для второго.

Поскольку тип String является векторным типом и не имеет фиксированного ограничения, мы не ввели никаких ограничений на количество символов, которые может содержать тема и содержимое. Мы только выделили правильный объем хранилища для них.

В настоящее время ничто не может помешать пользователю определить тему из 280 символов и содержание из 50 символов. Хуже того, поскольку для кодирования большинства символов требуется только один байт, и ничто не заставляет нас вводить тему, у нас может быть контент длиной (280 + 50) * 4 = 1320 символов.

Поэтому, если мы хотим защитить себя от этих сценариев, нам нужно добавить несколько охранников.

Давайте добавим пару операторов if, прежде чем увлажнять нашу учетную запись tweet. Мы проверим, что длина аргументов topic и content не превышает 50 и 280 символов соответственно. Мы можем получить доступ к количеству символов, содержащихся в строке, через my_string.chars().count(). Обратите внимание, что мы не используем my_string.len(), который возвращает длину вектора и, следовательно, дает нам количество байтов в строке.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;
    let author: &Signer = &ctx.accounts.author;
    let clock: Clock = Clock::get().unwrap();

    if topic.chars().count() > 50 {
        // Return a error...
    }

    if content.chars().count() > 280 {
        // Return a error...
    }

    tweet.author = *author.key;
    tweet.timestamp = clock.unix_timestamp;
    tweet.topic = topic;
    tweet.content = content;
    Ok(())
}

Теперь, когда операторы if на месте, нам нужно вернуть внутри них ошибку, чтобы досрочно остановить выполнение инструкции.

Anchor упрощает работу с ошибками, позволяя нам определить перечисление ErrorCode с помощью атрибута Rust #[error] . Для каждого типа ошибки внутри перечисления мы можем предоставить атрибут #[msg("...")], который объясняет это.

Давайте реализуем собственное перечисление ErrorCode и определим в нем две ошибки. Один, когда тема слишком длинная, и один, когда содержание слишком длинное.

Вы можете скопировать/вставить следующий код в конец файла lib.rs.

#[error]
pub enum ErrorCode {
    #[msg("The provided topic should be 50 characters long maximum.")]
    TopicTooLong,
    #[msg("The provided content should be 280 characters long maximum.")]
    ContentTooLong,
}

Now, let's use the errors we've just defined inside our if statements.

pub fn send_tweet(ctx: Context<SendTweet>, topic: String, content: String) -> ProgramResult {
    let tweet: &mut Account<Tweet> = &mut ctx.accounts.tweet;
    let author: &Signer = &ctx.accounts.author;
    let clock: Clock = Clock::get().unwrap();

    if topic.chars().count() > 50 {
        return Err(ErrorCode::TopicTooLong.into())
    }

    if content.chars().count() > 280 {
        return Err(ErrorCode::ContentTooLong.into())
    }

    tweet.author = *author.key;
    tweet.timestamp = clock.unix_timestamp;
    tweet.topic = topic;
    tweet.content = content;
    Ok(())
}

Как видите, сначала нам нужно получить доступ к типу ошибки как к константе — например. ErrorCode::TopicTooLong — и оберните его внутри типа перечисления Err. [Метод into()(https://doc.rust-lang.org/rust-by-example/conversion/from_into.html) — это функция Rust, которая преобразует наш тип ErrorCode в любой требуемый тип. по коду, который здесьErr, а точнее ProgramError`.

Отлично, мы не только защищены от недопустимых размеров тем и контента, но также знаем, как добавить больше типов ошибок и средств защиты в будущем.

Инструкции против транзакций

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

Однако разница проста: транзакция состоит из одной или нескольких инструкций.

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

Инструкции также могут делегироваться другим инструкциям внутри той же программы или за пределами текущей программы. Последний называется Cross-Program Invocations (CPI), и подписавшими текущую инструкцию являются автоматически передается во вложенные инструкции. У Anchor даже есть полезный API для вызовов CPI.

Независимо от того, сколько инструкций и вложенных инструкций существует внутри транзакции, она всегда будет атомарной — т. е. все или ничего.

Хотя мы не использовали и не будем напрямую использовать множественные и вложенные инструкции для каждой транзакции в этой серии, мы уже использовали их косвенно. При использовании ограничения учетной записи init от Anchor мы попросили Anchor инициализировать новую учетную запись для нас, и он сделал это, вызвав инструкцию create_account системной программы Solana, таким образом создав CPI.

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

Вывод

Хотите верьте, хотите нет, но наша программа Солана завершена! 🥳

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

Просмотреть эпизод 4 на GitHub

Сравните с Эпизодом 3

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

В конце концов, мы сделаем это в полноценном приложении JavaScript, но сначала сделаем это в тестах, чтобы убедиться, что все работает правильно. Увидимся в следующем выпуске!