Как стать автором
Обновить

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

Уровень сложностиСредний
Время на прочтение5 мин
Количество просмотров12K

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

Ссылки

Мой тг

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

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

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

Теги:
Хабы:
Всего голосов 8: ↑8 и ↓0+8
Комментарии5

Публикации

Истории

Работа

Ближайшие события