Как написать нативную программу Rust

Чтобы писать программы Solana без использования фреймворка Anchor, мы используем solana_program контейнер. Это базовая библиотека для написания ончейн программ на Rust.

Для начинающих рекомендуется начинать с фреймворка Anchor.

Программа #

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

lib.rs
use borsh::{BorshDeserialize, BorshSerialize};
use solana_program::{
    account_info::{next_account_info, AccountInfo},
    entrypoint,
    entrypoint::ProgramResult,
    msg,
    program::invoke,
    pubkey::Pubkey,
    rent::Rent,
    system_instruction::create_account,
    sysvar::Sysvar,
};
 
entrypoint!(process_instruction);
 
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = Instructions::try_from_slice(instruction_data)?;
    match instruction {
        Instructions::Initialize { data } => process_initialize(program_id, accounts, data),
    }
}
 
pub fn process_initialize(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: u64,
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
 
    let new_account = next_account_info(accounts_iter)?;
    let signer = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;
 
    let account_data = NewAccount { data };
    let size = account_data.try_to_vec()?.len();
    let lamports = (Rent::get()?).minimum_balance(size);
 
    invoke(
        &create_account(
            signer.key,
            new_account.key,
            lamports,
            size as u64,
            program_id,
        ),
        &[signer.clone(), new_account.clone(), system_program.clone()],
    )?;
 
    account_data.serialize(&mut *new_account.data.borrow_mut())?;
    msg!("Changed data to: {:?}!", data);
    Ok(())
}
 
#[derive(BorshSerialize, BorshDeserialize)]
pub enum Instructions {
    Initialize { data: u64 },
}
 
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct NewAccount {
    pub data: u64,
}
 

Точка входа (Entrypoint) #

Каждая программа Solana включает одну точку входа (entrypoint), используемую для запуска программы. Функция process_instruction используется для обработки данных, передаваемых в точку входа. Функция требует следующие параметры:

  • program_id - адрес выполняющейся в данный момент программы.
  • accounts - массив учетных записей, необходимых для выполнения инструкции.
  • instruction_data - сериализованные данные, специфичные для инструкции.
entrypoint!(process_instruction);
 
pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    ...
}

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

Инструкции #

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

#[derive(BorshSerialize, BorshDeserialize)]
pub enum Instructions {
    Initialize { data: u64 },
}

Переданное instruction_data в точку входа десериализуется для определения соответствующего варианта перечисления enum.

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = Instructions::try_from_slice(instruction_data)?;
    match instruction {
        Instructions::Initialize { data } => process_initialize(program_id, accounts, data),
    }
}

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

pub fn process_instruction(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    instruction_data: &[u8],
) -> ProgramResult {
    let instruction = Instructions::try_from_slice(instruction_data)?;
    match instruction {
        Instructions::Initialize { data } => process_initialize(program_id, accounts, data),
    }
}
 
pub fn process_initialize(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: u64,
) -> ProgramResult {
    ...
    Ok(())
}

Инструкция процесса #

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

pub fn process_initialize(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: u64,
) -> ProgramResult {
    let accounts_iter = &mut accounts.iter();
 
    let new_account = next_account_info(accounts_iter)?;
    let signer = next_account_info(accounts_iter)?;
    let system_program = next_account_info(accounts_iter)?;
 
    let account_data = NewAccount { data };
    let size = account_data.try_to_vec()?.len();
    let lamports = (Rent::get()?).minimum_balance(size);
 
    invoke(
        &create_account(
            signer.key,
            new_account.key,
            lamports,
            size as u64,
            program_id,
        ),
        &[signer.clone(), new_account.clone(), system_program.clone()],
    )?;
 
    account_data.serialize(&mut *new_account.data.borrow_mut())?;
    msg!("Changed data to: {:?}!", data);
    Ok(())
}

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

let accounts_iter = &mut accounts.iter();
 
let new_account = next_account_info(accounts_iter)?;
let signer = next_account_info(accounts_iter)?;
let system_program = next_account_info(accounts_iter)?;

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

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

let account_data = NewAccount { data };
let size = account_data.try_to_vec()?.len();
let lamports = (Rent::get()?).minimum_balance(size);
 
invoke(
    &create_account(
        signer.key,      // payer
        new_account.key, // new account address
        lamports,        // rent
        size as u64,     // space
        program_id,      // program owner address
    ),
    &[signer.clone(), new_account.clone(), system_program.clone()],
)?;

После успешного создания учетной записи, последним шагом является сериализация данных в поле data нового аккаунта. Это эффективно инициализирует данные аккаунта, сохраняя data переданные в точку входа программы.

account_data.serialize(&mut *new_account.data.borrow_mut())?;

Состояние #

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

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

#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct NewAccount {
    pub data: u64,
}

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

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

pub fn process_initialize(
    program_id: &Pubkey,
    accounts: &[AccountInfo],
    data: u64,
) -> ProgramResult {
 
    let account_data = NewAccount { data };
 
    invoke(
        ...
    )?;
 
    account_data.serialize(&mut *new_account.data.borrow_mut())?;
    msg!("Changed data to: {:?}!", data);
    Ok(())
}
...
 
#[derive(BorshSerialize, BorshDeserialize, Debug)]
pub struct NewAccount {
    pub data: u64,
}

Клиент #

Взаимодействие с программами Solana, написанными на нативном Rust, включает в себя непосредственное создание TransactionInstruction.

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

Info

Поддерживаются разные языки клиентов. Подробности для Rust и Javascript/Typescript можно найти в разделе «Клиенты Solana» документации.

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

native.test.ts
describe("Test", () => {
  it("Initialize", async () => {
    // Generate keypair for the new account
    const newAccountKp = new web3.Keypair();
 
    const instructionIndex = 0;
    const data = 42;
 
    // Create instruction data buffer
    const instructionData = Buffer.alloc(1 + 8);
    instructionData.writeUInt8(instructionIndex, 0);
    instructionData.writeBigUInt64LE(BigInt(data), 1);
 
    const instruction = new web3.TransactionInstruction({
      keys: [
        {
          pubkey: newAccountKp.publicKey,
          isSigner: true,
          isWritable: true,
        },
        {
          pubkey: pg.wallet.publicKey,
          isSigner: true,
          isWritable: true,
        },
        {
          pubkey: web3.SystemProgram.programId,
          isSigner: false,
          isWritable: false,
        },
      ],
      programId: pg.PROGRAM_ID,
      data: instructionData,
    });
 
    const transaction = new web3.Transaction().add(instruction);
 
    const txHash = await web3.sendAndConfirmTransaction(
      pg.connection,
      transaction,
      [pg.wallet.keypair, newAccountKp],
    );
    console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);
 
    // Fetch Account
    const newAccount = await pg.connection.getAccountInfo(
      newAccountKp.publicKey,
    );
 
    // Deserialize Account Data
    const deserializedAccountData = borsh.deserialize(
      AccountDataSchema,
      AccountData,
      newAccount.data,
    );
 
    console.log(Number(deserializedAccountData.data));
  });
});
 
class AccountData {
  data = 0;
  constructor(fields: { data: number }) {
    if (fields) {
      this.data = fields.data;
    }
  }
}
 
const AccountDataSchema = new Map([
  [AccountData, { kind: "struct", fields: [["data", "u64"]] }],
]);

Вызвать инструкции #

Чтобы вызвать инструкцию, вы должны вручную создать TransactionInstruction в соответствии с on-chain программой. Это включает указание:

  • Идентификатор вызываемой программы
  • AccountMeta для каждого аккаунта, требуемого инструкцией
  • Буфер данных инструкций, требуемый в инструкции
// Generate keypair for the new account
const newAccountKp = new web3.Keypair();
 
const instructionIndex = 0;
const data = 42;
 
// Create instruction data buffer
const instructionData = Buffer.alloc(1 + 8);
instructionData.writeUInt8(instructionIndex, 0);
instructionData.writeBigUInt64LE(BigInt(data), 1);
 
const instruction = new web3.TransactionInstruction({
  keys: [
    {
      pubkey: newAccountKp.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: pg.wallet.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: web3.SystemProgram.programId,
      isSigner: false,
      isWritable: false,
    },
  ],
  programId: pg.PROGRAM_ID,
  data: instructionData,
});

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

// Generate keypair for the new account
const newAccountKp = new web3.Keypair();

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

const instructionIndex = 0;
const data = 42;
 
// Create instruction data buffer
const instructionData = Buffer.alloc(1 + 8);
instructionData.writeUInt8(instructionIndex, 0);
instructionData.writeBigUInt64LE(BigInt(data), 1);

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

const instruction = new web3.TransactionInstruction({
  keys: [
    {
      pubkey: newAccountKp.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: pg.wallet.publicKey,
      isSigner: true,
      isWritable: true,
    },
    {
      pubkey: web3.SystemProgram.programId,
      isSigner: false,
      isWritable: false,
    },
  ],
  programId: pg.PROGRAM_ID,
  data: instructionData,
});

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

const transaction = new web3.Transaction().add(instruction);
 
const txHash = await web3.sendAndConfirmTransaction(
  pg.connection,
  transaction,
  [pg.wallet.keypair, newAccountKp],
);
console.log(`Use 'solana confirm -v ${txHash}' to see the logs`);

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

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

class AccountData {
  data = 0;
  constructor(fields: { data: number }) {
    if (fields) {
      this.data = fields.data;
    }
  }
}
 
const AccountDataSchema = new Map([
  [AccountData, { kind: "struct", fields: [["data", "u64"]] }],
]);

Затем получите AccountInfo для учетной записи, используя её адрес.

const newAccount = await pg.connection.getAccountInfo(newAccountKp.publicKey);

И в конце, десериализуйте поле AccountInfo, используя предопределенную схему.

const deserializedAccountData = borsh.deserialize(
  AccountDataSchema,
  AccountData,
  newAccount.data,
);
 
console.log(Number(deserializedAccountData.data));