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). Если я увижу положительную обратную связь, то вторая часть не заставит себя долго ждать
Ссылки
ссылка на оригинал статьи https://habr.com/ru/post/716312/
Добавить комментарий