Program
Начнем с определения того, что такое "Solana program" - именно так в блокчейне обозначаются смарт-контракты. Это исполняемый код интерпретирующий проходящие через него инструкции, которые в свою очередь являются частью любой транзакции в сети Solana.
Instruction
Инструкция содержит в себе:
Program id - адрес программы, с которой будет взаимодействовать транзакция
Accounts - аккаунты, которые программа сможет обрабатывать
Data - произвольный набор байтов
System program
Это нативная программа через которую происходит перевод sol, создание аккаунтов и т.д. Подробнее про нативные программы в официальной доке
Переводим sol
Инициализируем проект
cargo init sol-transfer --lib
Добавляем крейт
cargo add solana-program
Импортируем модули в lib.rs
account_info::AccountInfo - структура данных описывающая аккаунт
account_info::next_account_info - функция позволяющая получить следующий элемент в итераторе с AccountInfo
entrypoint - указывает на функцию-обработчик инструкции
entrypoint::ProgramResult - тип который возвращает функция-обработчик инструкции
program::invoke - функция для вызова межпрограммных инструкций
program_error::ProgramError - enum с ошибками программы
pubkey::Pubkey - структура данных описывающая публичный адрес аккаунта
system_instruction - модуль для взаимодействия с System program. В нашем случае получаем структуру для перевода lamports (Дробная часть Соланы)
use solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint,
entrypoint::ProgramResult,
program::invoke,
program_error::ProgramError,
pubkey::Pubkey,
system_instruction,
};
Указываем entrypoint
entrypoint!(process_instruction);
Создаем функцию-обработчик инструкций
Аргументы
program_id - публичный адрес программы
accounts - массив аккаунтов, который был передан в инструкции
instruction_data - произвольный набор байтов
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
Аккаунты
Создаем итератор и объявляем переменные с аккаунтами отправителя и получателя
let accounts_iter = &mut accounts.iter();
let sender = next_account_info(accounts_iter)?;
let destination = next_account_info(accounts_iter)?;
Instruction data
Обычно для передачи данных между клиентом и программой используются сериализация структур данных через библиотеку borsh. Подробнее про это в документации. Но так как нам в дате нужно передавать только число переводимых lamports, то можно обойтись и без этого
Десериализируем набор байтов и получаем кол-во переводимых монет
let amount = instruction_data
.get(..8)
.and_then(|slice| slice.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(ProgramError::InvalidInstructionData)?;
System program и Transfer
Получаем инструкцию для перевода lamports
sender.key - публичный адрес отправителя
destination.key - публичный адрес получателя
amount - кол-во переводимых lamports
let transfer_instruction = system_instruction::transfer(
sender.key,
destination.key,
amount
);
Вызываем межпрограммную инструкцию на перевод sol через System program. Не забываем передать аккаунты, которые участвуют в трансфере
invoke(
&transfer_instruction,
&[sender.clone(), destination.clone()],
)?;
Готовый контракт
Соединяем все воедино и возвращаем Ok(())
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let accounts_iter = &mut accounts.iter();
let sender = next_account_info(accounts_iter)?;
let destination = next_account_info(accounts_iter)?;
let amount = instruction_data
.get(..8)
.and_then(|slice| slice.try_into().ok())
.map(u64::from_le_bytes)
.ok_or(ProgramError::InvalidInstructionData)?;
let transfer_instruction = system_instruction::transfer(
sender.key,
destination.key,
amount
);
invoke(
&transfer_instruction,
&[sender.clone(), destination.clone()],
)?;
Ok(())
}
Деплоим программу в localnet
Компилируем в .so
cargo-build-bpf
Запускаем локальную сеть
Команда запускает локальный валидатор и JSON RPC(HTTP API)
solana-test-validator
Деплоим .so файл
Загружаем смарт-контракт в сеть и получаем адрес его аккаунта(Program Id)
solana program deploy target/deploy/transfer_program.so
Тестируем с браузера
Импортируем Solana web3.js
Это веб библиотека для взаимодействия с блокчейном Solana через RPC API и встроенные функции. Подробнее в доке
script = document.createElement('script');
script.src = 'https://unpkg.com/@solana/web3.js@latest/lib/index.iife.js';
document.body.append(script);
Создаем объект соединения
rpcUrl - наш локальный JSON RPC эндпоинт
let rpcUrl = 'http://127.0.0.1:8899';
let connection = new solanaWeb3.Connection(rpcUrl);
Инициализируем кошельки отправителя и получателя
let sender = solanaWeb3.Keypair();
let destination = solanaWeb3.Keypair();
Запрашиваем airdrop 1 SOL на аккаунт отправителя
solanaWeb3.LAMPORTS_PER_SOL - кол-во lamports в одном sol
await connection.requestAirdrop(sender.publicKey, solanaWeb3.LAMPORTS_PER_SOL);
Объявляем переменную с публичным адресом программы
Этот адрес мы получали, когда деплоили контракт в сеть
let programId = new solanaWeb3.PublicKey('YOUR_PROGRAM_ADDRESS')
Транзакция
Создаем объект транзакции
let transaction = new solanaWeb3.Transaction();
Transaction data
Как я уже говорил, обычно данные сериализуют через определенные библиотеки, но перегнать число в массив байтов можно и чистым JavaScript. Вот например функция, которую я написал. Углубляться в то как она работает я не буду
function BytesFromInt(n) {let a=[];let fir=n%2**8;n-=fir;a.push(fir);let s=~~(n%2**16/2**8);n-=s;a.push(s);let t=~~(n%2**24/2**16);n-=t;a.push(t);let fo=~~(n%2**32/2**24);n-=fo;a.push(fo);let fif=~~(n%2**40/2**32);n-=fif;a.push(fif);return a.concat([0,0,0]);}
Добавляем инструкцию
isSigner: true - этот аккаунт подписывает транзакцию своим приватным ключом, а так же платит комиссию
isWritable: true - состояние этого аккаунта может изменяться программой
transaction.add(new solanaWeb3.TransactionInstruction({
keys: [{
// Отправитель
pubkey: sender.publicKey,
isWritable: true,
isSigner: true
}, {
// Получатель
pubkey: destination.publicKey,
isWritable: true,
isSigner: false
}, {
// System program
pubkey: solanaWeb3.SystemProgram.programId,
isWritable: false,
isSigner: false
}],
// Адрес нашей программы
programId: programId,
// Кол-во sol, которое мы переводим - 0.1 sol
data: BytesFromInt(0.1*solanaWeb3.LAMPORTS_PER_SOL)
}));
Отправляем!
Последний аргумент тут - это массив пар ключей, которые подписывают транзакцию
await solanaWeb3.sendAndConfirmTransaction(connection, transaction, [sender]);
Проверяем баланс
Проверяем баланс аккаунта получателя. Если все прошло успешно, вызов вернет 100000000(lamports)
await connection.getBalance(destination.publicKey);
Заключение
В этой статье я попытался максимально просто и понятно донести теорию работы блокчейна Solana и на практике показал, как с нуля написать программу перевода sol между двумя кошельками. На самом деле блокчейн разработка - это огромный и безумно интересный пласт знаний, и многие важные темы я не затронул: PDA аккаунты, SPL токены, сериализация структур данных, безопасность(и это только Solana). Если я увижу положительную обратную связь, то вторая часть не заставит себя долго ждать
Ссылки
Комментарии (3)
Iney61
00.00.0000 00:00+1Большое спасибо. Очень познавательная статья. Я сейчас разбираюсь Solana- Rust- Anchor и испытываю определенные трудности. Особенно с Anchor. Интересно было бы почитать в подобном стиле про Anchor.
excentro
Спасибо, интересно. Наверное, сейчас rust используется в большей степени именно для блокчейна.
MatveySss Автор
Спасибо за комментарий. Да в основном Rust сейчас используется для блокчейн разработки. Еще я часто слышу о том, что на нем пишут/переписывают определенные высоконагруженные части приложений