Начало работы с фреймворком Anchor

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

Думайте об Anchor как о фреймворке для программ Solana, так же как Next.js для веб- разработки. Подобно тому, как Next.js позволяет разработчикам создавать веб-сайты с использованием React вместо того, чтобы полагаться исключительно на HTML и TypeScript, Anchor предоставляет набор инструментов и абстракций, которые делают создание программ Solana более интуитивно понятным и безопасным.

Основные макросы, встречающиеся в программе Anchor, включают:

  • declare_id: Определяет адрес программы on-chain
  • #[program]: Определяет модуль, содержащий логику инструкций программы
  • #[derive(Accounts)]: Применяется к структурам для указания списка аккаунтов, необходимых для выполнения инструкции
  • #[account]: Применяется к структурам для создания пользовательских типов учетных записей, специфичных для программы

Anchor программа #

Ниже приведена простая Anchor программа с одной инструкцией, которая создает новую учетную запись. Мы пройдем по ней, чтобы объяснить базовую структуру Anchor программы. Вы можете запустить этот пример на Solana Playground.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

declare_id макрос #

Для задания on-chain адреса программы (ID программы) используется макрос declare_id.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");

Когда вы впервые создаете программу Anchor, платформа генерирует новую пару ключей, используемую для развертывания программы (если не указано иное). Открытый ключ из этой пары ключей следует использовать в качестве идентификатора программы в declare_id макросе.

  • При использовании Solana Playground идентификатор программы обновляется автоматически и может быть экспортирован с помощью пользовательского интерфейса.
  • При локальной сборке пару ключей программы можно найти в /target/deployment /your_program_name.json

program макрос #

В макросе #[program] указывает модуль, содержащий все инструкции программы. Каждая публичная функция в модуле представляет отдельную инструкцию для программы.

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

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

Тип Context предоставляет инструкции доступ к следующим входам без аргументов:

pub struct Context<'a, 'b, 'c, 'info, T> {
    /// Currently executing program id.
    pub program_id: &'a Pubkey,
    /// Deserialized accounts.
    pub accounts: &'b mut T,
    /// Remaining accounts given but not deserialized or validated.
    /// Be very careful when using this directly.
    pub remaining_accounts: &'c [AccountInfo<'info>],
    /// Bump seeds found during constraint validation. This is provided as a
    /// convenience so that handlers don't have to recalculate bump seeds or
    /// pass them in as arguments.
    pub bumps: BTreeMap<String, u8>,
}

Context — это универсальный тип, в котором T представляет набор учетных записей, требуемых инструкции. При определении инструкции Context типом T является структура, реализующая Accounts признак (Context<Initialize>).

Этот параметр контекста позволяет инструкции получить доступ:

  • ctx.accounts: Аккаунты инструкции
  • ctx.program_id: Адрес самой программы
  • ctx.remaining_accounts: Все оставшиеся аккаунты, предоставленные инструкции но не указанные в структуре Accounts
  • ctx.bumps: Пополнить seeds для любых Программных адресов (PDA) аккаунтов, указанные в структуре Accounts

макрос derive(Accounts) #

Макрос #[derive(Accounts)] применяется к структуре и реализует Accounts признак. Используется для определения и проверки набора аккаунтов, необходимых для конкретной инструкции.

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

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

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

При создании программ Solana важно проверять учетные записи, предоставленные клиентом. Эта проверка достигается в Anchor посредством ограничений учетной записи и указания соответствующих типов учетной записи:

  • Account Constraints: Ограничения определяют дополнительные условия, которым должна удовлетворять учетная запись, чтобы считаться действительной для инструкции. Ограничения применяются с использованием атрибута #[account(..)], который помещается выше поля учетной записи в структуре Accounts.

    #[derive(Accounts)]
    pub struct Initialize<'info> {
        #[account(init, payer = signer, space = 8 + 8)]
        pub new_account: Account<'info, NewAccount>,
        #[account(mut)]
        pub signer: Signer<'info>,
        pub system_program: Program<'info, System>,
    }
  • Account Types: Anchor предоставляет различные типы учетных записей, чтобы гарантировать, что учетная запись, предоставленная клиентом, соответствует ожиданиям программы.

    #[derive(Accounts)]
    pub struct Initialize<'info> {
        #[account(init, payer = signer, space = 8 + 8)]
        pub new_account: Account<'info, NewAccount>,
        #[account(mut)]
        pub signer: Signer<'info>,
        pub system_program: Program<'info, System>,
    }

Учетные записи в структуре Accounts доступны в инструкции через Context, используя синтаксис ctx.accounts.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

Когда вызывается инструкция в программе Anchor, программа выполняет следующие проверки, как указано в Accounts структуре:

  • Проверка типа учетной записи: проверяет, что учетные записи, переданные в инструкцию, соответствуют типам учетных записей, определенным в Контексте инструкции.

  • Проверки ограничений: он проверяет учетные записи на соответствие любым указанным дополнительным ограничениям.

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

Более подробные примеры см. в разделах ограничения и типы учетных записей документации Anchor.

account макрос #

Макрос #[account] применяется к структурам для определения формата пользовательского типа учетной записи данных для программы. Каждое поле в структуре представляет поле, которое будет храниться в данных аккаунта.

#[account]
pub struct NewAccount {
    data: u64,
}

Этот макрос реализует различные черты описанные здесь. Ключевые функции макроса `#[account] включают:

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

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

#[account(init, payer = signer, space = 8 + 8)]
pub new_account: Account<'info, NewAccount>,

Дискриминатор используется в ходе двух сценариев:

  • Инициализация: В процессе инициализации учётной записи дискриминатор устанавливается с помощью дискриминатора типа аккаунта.
  • Десериализация: Когда данные учетной записи десериализованы, дискриминатор в рамках проверяется на соответствие ожидаемого дискриминатора типа аккаунта.

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

IDL файл #

Когда программа Anchor создается, Anchor генерирует файл языка описания интерфейса (IDL), представляющий структуру программы. Этот IDL файл предоставляет стандартизированный формат на основе JSON для создания программных инструкций и получения учетных записей программ.

Ниже приведены примеры того, как IDL файл связан с программным кодом.

Инструкции #

Массив instructions в IDL соответствует инструкциям программы и задает необходимые учетные записи и параметры для каждой инструкции.

IDL.json
{
  "version": "0.1.0",
  "name": "hello_anchor",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        { "name": "newAccount", "isMut": true, "isSigner": true },
        { "name": "signer", "isMut": true, "isSigner": true },
        { "name": "systemProgram", "isMut": false, "isSigner": false }
      ],
      "args": [{ "name": "data", "type": "u64" }]
    }
  ],
  "accounts": [
    {
      "name": "NewAccount",
      "type": {
        "kind": "struct",
        "fields": [{ "name": "data", "type": "u64" }]
      }
    }
  ]
}
lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}

Аккаунты #

Массив accounts в IDL соответствует структурам в программе, аннотированным макросом `#[account], который определяет структуру аккаунтов данных программы.

IDL.json
{
  "version": "0.1.0",
  "name": "hello_anchor",
  "instructions": [
    {
      "name": "initialize",
      "accounts": [
        { "name": "newAccount", "isMut": true, "isSigner": true },
        { "name": "signer", "isMut": true, "isSigner": true },
        { "name": "systemProgram", "isMut": false, "isSigner": false }
      ],
      "args": [{ "name": "data", "type": "u64" }]
    }
  ],
  "accounts": [
    {
      "name": "NewAccount",
      "type": {
        "kind": "struct",
        "fields": [{ "name": "data", "type": "u64" }]
      }
    }
  ]
}
lib.rs
use anchor_lang::prelude::*;
 
declare_id!("11111111111111111111111111111111");
 
#[program]
mod hello_anchor {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>, data: u64) -> Result<()> {
        ctx.accounts.new_account.data = data;
        msg!("Changed data to: {}!", data);
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(init, payer = signer, space = 8 + 8)]
    pub new_account: Account<'info, NewAccount>,
    #[account(mut)]
    pub signer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[account]
pub struct NewAccount {
    data: u64,
}
 

Клиент #

Anchor предоставляет библиотеку Typescript (@coral-xyz/anchor), которая упрощает процесс взаимодействия с программами Solana из клиента.

Для использования клиентской библиотеки необходимо сначала настроить экземпляр Program с использованием IDL файла, созданного Anchor.

Клиентская программа #

Создание экземпляра Program требует IDL программы, его on-chain адрес (programId), а также AnchorProvider. AnchorProvider объединяет две вещи:

  • Connection - соединение с кластером Solana (т.е. localhost, devnet, mainnet)
  • Wallet - (опционально) кошелек по умолчанию, используемый для оплаты и подписания транзакций

При локальной сборке программы Anchor настройка создания экземпляра Program выполняется автоматически в тестовом файле. IDL файл можно найти в папке /target.

import * as anchor from "@coral-xyz/anchor";
import { Program, BN } from "@coral-xyz/anchor";
import { HelloAnchor } from "../target/types/hello_anchor";
 
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.HelloAnchor as Program<HelloAnchor>;

При интеграции с фронтендом с помощью wallet adapter, вам потребуется вручную настроитьAnchorProvider и Program.

import { Program, Idl, AnchorProvider, setProvider } from "@coral-xyz/anchor";
import { useAnchorWallet, useConnection } from "@solana/wallet-adapter-react";
import { IDL, HelloAnchor } from "./idl";
 
const { connection } = useConnection();
const wallet = useAnchorWallet();
 
const provider = new AnchorProvider(connection, wallet, {});
setProvider(provider);
 
const programId = new PublicKey("...");
const program = new Program<HelloAnchor>(IDL, programId);

Альтернативно вы можете создать экземпляр Program, используя только IDL и соединение Connection с кластером Solana. Это означает, что значение по умолчанию отсутствует для Wallet, но позволяет использовать Program для получения учетных записей до подключения кошелька.

import { Program } from "@coral-xyz/anchor";
import { clusterApiUrl, Connection, PublicKey } from "@solana/web3.js";
import { IDL, HelloAnchor } from "./idl";
 
const programId = new PublicKey("...");
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
 
const program = new Program<HelloAnchor>(IDL, programId, {
  connection,
});

Инструкции вызова #

Как только Program настроен, вы можете использовать [MethodsBuilder](https://github. om/coral-xyz/anchor/blob/852fcc77beb6302474a11e0f8e6f1e688021be36/ts/packages/anchor/src/program/namespace/methods s.ts#L155) в Anchor для создания инструкции, транзакции, или создания и отправки транзакции. Базовый формат выглядит так:

  • program.methods - Это API конструктора для создания инструкций вызова связанных с IDL программы
  • .instructionName - Специальная инструкция от программы IDL, передающая любые данные инструкции как значения, разделенные запятыми
  • .accounts - Передайте адрес каждой учетной записи, требуемый инструкцией, как указано в IDL
  • .signers - Опционально передайте массив пар ключей, необходимых в качестве дополнительных подписывающих лиц по инструкции
await program.methods
  .instructionName(instructionData1, instructionData2)
  .accounts({})
  .signers([])
  .rpc();

Ниже приведены примеры того, как вызвать инструкцию с помощью конструктора методов.

rpc() #

Метод rpc() отправляет подписанную транзакцию с указанной инструкцией и возвращает файл TransactionSignature. При использовании .rpc, Wallet из Provider автоматически включается в качестве подписывающей стороны.

// Generate keypair for the new account
const newAccountKp = new Keypair();
 
const data = new BN(42);
const transactionSignature = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .signers([newAccountKp])
  .rpc();

transaction() #

Метод transaction() создает транзакцию и добавляет указанную инструкцию в транзакцию (без автоматической отправки).

// Generate keypair for the new account
const newAccountKp = new Keypair();
 
const data = new BN(42);
const transaction = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .transaction();
 
const transactionSignature = await connection.sendTransaction(transaction, [
  wallet.payer,
  newAccountKp,
]);

instruction() #

Метод instruction() создает TransactionInstruction с использованием указанной инструкции. Это полезно, если вы хотите вручную добавить инструкцию к транзакции и объединить ее с другими инструкциями.

// Generate keypair for the new account
const newAccountKp = new Keypair();
 
const data = new BN(42);
const instruction = await program.methods
  .initialize(data)
  .accounts({
    newAccount: newAccountKp.publicKey,
    signer: wallet.publicKey,
    systemProgram: SystemProgram.programId,
  })
  .instruction();
 
const transaction = new Transaction().add(instruction);
 
const transactionSignature = await connection.sendTransaction(transaction, [
  wallet.payer,
  newAccountKp,
]);

Получить аккаунты #

Клиент Program также позволяет легко получать и фильтровать программы аккаунтов. Просто используйте program.account и укажите имя типа аккаунта в IDL. Затем Anchor десериализует и возвращает все учетные записи, как указано.

all() #

Используйте all() чтобы получить все существующие аккаунты для определенного типа.

const accounts = await program.account.newAccount.all();

memcmp #

Используйте memcmp для фильтрации данных аккаунтов, соответствующих определенному значению с определенным смещением. При расчете смещения, помните, что первые 8 байт зарезервированы для дискриминатора аккаунта в учетных записях, созданных с помощью программы Anchor. Использование memcmp требует понимания разметки байтов поля данных для типа учетной записи, который вы получаете.

const accounts = await program.account.newAccount.all([
  {
    memcmp: {
      offset: 8,
      bytes: "",
    },
  },
]);

fetch() #

Используйте fetch() для получения данных аккаунта для конкретной учетной записи путем передачи адреса учетной записи

const account = await program.account.newAccount.fetch(ACCOUNT_ADDRESS);

fetchMultiple() #

Используйте fetchMultiple() чтобы получить данные учетной записи для нескольких аккаунтов, передав адресов учетных записей

const accounts = await program.account.newAccount.fetchMultiple([
  ACCOUNT_ADDRESS_ONE,
  ACCOUNT_ADDRESS_TWO,
]);