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). Если я увижу положительную обратную связь, то вторая часть не заставит себя долго ждать
