Solana смарт-контракты на Rust для самых маленьких

от автора

Program

Начнем с определения того, что такое «Solana program» — именно так в блокчейне обозначаются смарт-контракты. Это исполняемый код интерпретирующий проходящие через него инструкции, которые в свою очередь являются частью любой транзакции в сети Solana.

Instruction

Инструкция содержит в себе:

  1. Program id — адрес программы, с которой будет взаимодействовать транзакция

  2. Accounts — аккаунты, которые программа сможет обрабатывать

  3. 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::ProgramErrorenum с ошибками программы

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

Ссылки

Мой тг

Документация Solana

Документация solana_program

Документация Solana web3.js


ссылка на оригинал статьи https://habr.com/ru/post/716312/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *