Чтобы писать программы Solana без использования фреймворка Anchor, мы используем
solana_program
контейнер. Это базовая библиотека для написания ончейн программ на Rust.
For beginners, it is recommended to start with the Anchor framework.
Программа #
Ниже приведена простая программа на Solana с единой инструкцией, создающей новый аккаунт. Мы пройдем по ней, чтобы объяснить базовую структуру программы на Solana. Вы можете запустить этот пример на Solana Playground.
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,
}
Клиент #
Interacting with Solana programs written in native Rust involves directly
building the
TransactionInstruction
.
Аналогичным образом, для получения и десериализации данных аккаунта требуется создание схемы совместимой со структурами данных онйчен программы.
Поддерживаются разные языки клиентов. Подробности для Rust и Javascript/Typescript можно найти в разделе "Клиенты Solana" документации.
Ниже мы рассмотрим пример того, как вызвать инструкцию initialize
из программы
выше.
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));