May 18, 2022

Как написать диспарсер на солане

Конечно солановский блокчейн немного отличается от блокчейна эфира, но в целом все примерно также работает.

Терминология

Программа - Смарт контракт только на солане.

Лампорты - Самая маленькая часть токена Солана, 1 лампорт ~ 0.000000001 sol. В них оцениваются все комиссии на транзакции

IDL - ABI только на солане. Нужно чтобы используя веб3 можно было через любой код взаимодействовать с программой.

Раст - язык программирования на котором написан блокчейн соланы, а также все программы.

Camel Case - верблюжий регистр, используется в джаваскрипте для обозначения функций и переменных. Пример: disperseSol

Snake Case - змеинный регистр, используется в расте для обозначения функций и переменных. Пример: disperse_sol

Что же понадобится для воиспроизведения всех следующих действий

Для написание самой программы нужно будет скачать раст, скачать его можно с официального сайта. Но по стольку по скольку это С-подобный язык, для него также надо скачать некоторые компоненты. (перейдя по этой ссылке автоматически начнется скачивание инсталлера нужных компонентов) После установления инсталлера, в него нужно будет зайти, и установить сами компоненты.

Нажимайте "Изменить"

Выбирайте данный модуль в левом верхнем углу

Нажимайте установить в правом нижнем углу и ждите окончания установки

Да, доп компоненты занимают не мало места, но на то раст и сделан, чтобы с безопасностью и со всеми другими преспособлениями можно было легко и удобно работать с памятью и т.д, но сейчас не об этом.

Если вы успешно сделали все выше указанные действия, то можете приступать к установке самого раста. Если вы работаете на виндовс, то вы должны были скачать exe инсталлер с официального сайта о котором мы говорили выше. При запуске ехе, если вы увидите данный текст:

Значит, вы правильно установили доп компоненты, и введя в консоль 1 и нажав Enter, начнется загрузка, по окончанию которой вам сообщат что возможно надо будет добавить саму основную директорию раста (.cargo/bin) в PATH окружения вашего компьютера. Обычно конечно раст все это делает сам, но это можно легко проверить. Если открыть новую консоль, найдя коммандную строку в поиске виндовс, и ввести туда:

cargo --version

Если вывод будет примерно похож на это (у вас возможно будет другая версия), то значит все работает и ничего менять не надо:

Если же в выводе будет ошибка, и будет написано что команды cargo не существует, то вам придется самому добавит ее в PATH. По этому поводу есть много гайдов в интернете, вот один пример, поэтому с этим проблем не должно быть.

После установки раста, надо будет обязательно установить сами cli компоненты соланы. Там в зависимости от вашей системы, устанавливайте все по инструкции. Возможно у вас будут проблемы с PATH окружения, но если что все описано на самом сайте, а также при установке вам сам инсталлер все выведет в консоль и скажет что точно надо сделать, чтобы добавить солану к PATH. Для проверки добавилась ли солана правильно в PATH, можно проверить ровно также как и раст, только соответственно заменяя cargo на solana:

solana --version

Также для провождения тестов ну и так скажем для взаимодействия с самими программами нужно скачать node.js, и несколько дополнительных модулей вместе с ним. С установкой node.js больших проблем не должно быть, вот хороший гайд как установить node.js на любую линовскую ОС, если же будут проблемы с установкой на прочие ОС то можно найти кучу гайдов на том же ютубе, поэтому проблем тут быть не должно.

Чтобы установить доп модули можно просто ввести данную команду в коммандную строку/консоль и дождаться пока все установится (учитывая что node вы установили правильно и он у вас работает):

npm install -g ts-node typescript yarn @project-serum/anchor-cli

Инициализация самой программы

В данном гайде мы решили использовать некоторые модули, чтобы упростить саму инициализацию программы, а также чтобы не надо было писать много лишнего и не понятного кода с нуля.

anchor init disperse

Вместо disperse можно ввести любое другое значение (это название самого проекта)

После скачивания всех дополнительных компонентов и инициализации проекта, у вас в директории в которой вы сейчас находитесь создадиться папка в нашем случае под названием disperse. В этой папке сразу будет весь нужный написанный код под саму программу, а также по тесты написанные в данном случае на typescript.

Объяснение всех созданных папок и файлов

Вот все папки и файлы которые должны были создаться при выполнении функции выше:

Anchor.toml - Общий конфиг самой программы

Тут оставляйте все точно также, кроме как самой программы, которая указана в занчении disperse, а так для тестов на локалнете можно оставить тот адресс который стоит там по дефолту.

Как получить адресс программы показано ниже в статье

Если же вы захотите деплоить на мейн или дев нет, то надо будет также поменять значение programs.localnet и cluster = "localnet" на programs.devnet/programs.mainnet и cluster = "devnet"/cluster = "mainnet" соответственно.

programs - Папка где находится сам код программы (programs/disperse/src/lib.rs), а также прочие папки и файлы с нужными модулями, но их трогать не надо, потому что anchor сам установил в них все нужные модули и значения при инициализации.

tests - Папка в которой находится сам код через который мы будем проводить тесты и подключаться к программе

target - Папка в которой хранится информация и деплой файл программы, а также IDL, который надо использовать чтобы можно было взаимодействовать с программой

На все остальные папки можно не обращать внимания, потому что в них либо же различные файлы для модулей которые мы установили раньше, либо же прочие системные и тестовые файлы.

Файлы программы и тестов и подробное объяснение

Сама программа:

Вот весь код который должен быть в файле lib.rs, если лень читать дальше можете просто скопировать и вставить, а так дальше будет подробное объяснение:

use anchor_lang::prelude::*;

declare_id!("A1qXKzEUEgh9xRs6RvFwKZzTxrQEshynQm7tdc3ALEPu");

#[program]
pub mod disperse {
    use super::*;

    pub fn disperse_sol<'info>(ctx: Context<'_, '_, '_, 'info, Initialize<'info>>, amount: u64) -> Result<()> {
        for address in ctx.remaining_accounts {
            anchor_lang::solana_program::program::invoke(
                &anchor_lang::solana_program::system_instruction::transfer(
                    &ctx.accounts.initializer.key(),
                    &address.key(),
                    amount,
                ),
                &[
                    ctx.accounts.initializer.to_account_info(),
                    address.to_account_info(),
                ],
            );
        };
        Ok(())
    }
}

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(mut)]
    pub initializer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

Сама функция раскидки соланы на разные кошельки можно увидеть на 9 строчке, под названием disperse_sol. Также в саму функцию мы передаем контекст, в котором находится сам инициализатор программы (кошелек с которого будет солана раскидываться), плюс в контексте также передается список кошельков на которые деньги будут переведены. А также в аргументах функции можно увидеть сколько соланы будет переведено на каждый кошелек - amount: u64, amount - само количество в Лампортах, а u64 - тип данных в нашем случае положительное число с максимальным значением 18446744073709551615 (64 бита), поэтому хоть Лампорты занимают очень маленькую часть одной соланы, через эту программу можно все равно раскидывать большое количество соланы.

В самой функции можно увидеть цикл который проходится по всем кошелькам, которые мы со своей программы должны указать в переменную remainingAccounts, но об этом чуть позже, а также внутри цикла - две другие функции - transfer - инициализация самой транзакции, в эту функцию надо передать три аргумента - из какого аккаунта, на какой, сколько лампортов; а также функция invoke - подписание самой транзакции чтобы она отправилась, в эту функцию надо передать всего два аргумента - саму транзакцию, и список в котором содержиться сериализованная информация об обоих аккаунтах (сериализация производится использую функцию .to_account_info(), о ней подробней можно почитать в документации). Ну и в самом конце функции можно увидеть оператор Ok(()), который просто символизирует успешное проведение транзакции.

В самом низу кода объявляется сам класс, который мы передаем в виде контекста в нашу функцию. initializer - Инициализатор программы, с которого списывается солана и переводится на другие аккаунты. system_program - просто переменная программы, на нее в принципе можно и не обращать внимание. И да, на сколько вы могли заметить, в этот класс мы не передаем значение remaining_accounts, это все потому что солана и anchor еще не супортят списки аккаунтов (их просто нужно в определенной форме передовать), поэтому надо использовать уже существующую переменную, которая автоматом заложена в контексте.

Имплементация и тесты программы через typescript:

Вот опять же таки весь код файла disperse.ts, а также дальше подробное объяснение:

import * as anchor from "@project-serum/anchor";
import { Program, BN } from "@project-serum/anchor";
import { Disperse } from "../target/types/disperse";

describe("disperse", () => {
  anchor.setProvider(anchor.Provider.env());

  const program = anchor.workspace.Disperse as Program<Disperse>;

  it("Is initialized!", async () => {
    const initializer = anchor.web3.Keypair.generate();

    const connection = await anchor.getProvider().connection;
    let signature = await connection.requestAirdrop(initializer.publicKey, 10*anchor.web3.LAMPORTS_PER_SOL)
    await connection.confirmTransaction(signature);

    console.log("Main account initial balance: ", await connection.getBalance(initializer.publicKey));

    const kp1 = anchor.web3.Keypair.generate();
    const kp2 = anchor.web3.Keypair.generate();
    const kp3 = anchor.web3.Keypair.generate();
    const kp4 = anchor.web3.Keypair.generate();
    const kp5 = anchor.web3.Keypair.generate();
    console.log("Secondary account initial balance: ", await connection.getBalance(kp1.publicKey));
    const tx = await program.rpc.disperseSol(new BN(1 * anchor.web3.LAMPORTS_PER_SOL), {
      accounts: {
        initializer: initializer.publicKey,
        systemProgram: anchor.web3.SystemProgram.programId
      },
      remainingAccounts: [
        { pubkey: kp1.publicKey, isWritable: true, isSigner: false },
        { pubkey: kp2.publicKey, isWritable: true, isSigner: false },
        { pubkey: kp3.publicKey, isWritable: true, isSigner: false },
        { pubkey: kp4.publicKey, isWritable: true, isSigner: false },
        { pubkey: kp5.publicKey, isWritable: true, isSigner: false },
      ],
      signers: [initializer]
    });
    console.log("Your transaction signature", tx);
    console.log("Main account final balance: ", await connection.getBalance(initializer.publicKey));
    console.log("Secondary account final balance: ", await connection.getBalance(kp1.publicKey));
  });
});

Сверху импортируются все нужные модули, а также сам IDL программы, дальше идет сама тестовая функция - describe, она будет работать только при тестах, потому что для этого используется специальный модуль. В ней указывается сама программа, а потом уже идет главная асинхронная функция в которой мы создаем главный и второстепенные кошельки, а также на главный кошелек аирдропаем 10 соланы, чтобы можно было провести сам тест. Так же в консоль выводятся для показания балансы кошельков до и после самой раскидки.

После создания кошельков, идет инициализация самой программы, через функцию которую мы указали в коде самой программы, только заметьте что тут все в camelCase, поэтому функция не disperse_sol а disperseSol. То же самое относится к указаннию переменной программы (systemProgram) и адресам кошельков на которые расикидывается солана (remainingAccounts).

Также заметьте, что количество соланы передается в виде Big Number - сериализация в бинарное значение.

В переменную accounts надо указать то что у нас указанно в контексте в коде самой программы, соответственно адрес самого инициализатора, ну и в переменную программы надо просто вставить то значение которое в самом начале кода мы назначини в переменную окружения.

Еще, самое главное - правильно указать адреса в remainingAccounts, опять же таки как мы писали выше, указывать кошельки туда надо в определенном формате, и хоть таким образом это делать не совсем удобно, можно используя функцию map() считать все значения кошельков и добавить их в новый список уже в нужном формате, но сейчас не об этом. Так вот что нужно указать: сам адресс кошелька, нужно ли его изменять в программе (isWritable) в нашем случае да, потому что нам надо на кошельки эти перевести солану, а также является ли кошелек подписчиком транзакции (isSigner) в нашем случае транзакцию подписывает только инициализатор, так как именно с главного аккаунта раскидывается солана, поэтому ставим false.

Ну и в самом конце нужно соответственно указать подписчика транзакции, в нашем случае - это инициализатор транзакции потому что с него как раз-таки переводится солана на другие кошельки.

Использ0вание программы

Да, конечно мы написали только тесты для программы, и только на локальной сети, но в интернете есть очень много гайдов и инструкций как имплементировать свои программы в свой код.

А так чтобы выполнить тесты нужно ввести данную функцию находясь в консоле в директории основной папки проекта:

anchor test

Таким образом сразу в локальной сети без всяких валидаторов и не заплатив ни одного цента вы сможете провести тесты над программой. В нашем случае выводится такое в консоль:

Если вы хотите задеплоить и запустить свою программу, вам сначала надо создать кошелек используя данную функцию:

solana-keygen new --output <ФАЙЛ В КОТОРОМ ХОТИТЕ СОХРАНИТЬ КОШЕЛЕК>

потом указать созданный файл в конфиге программы (файл Anchor.toml), а также чтобы задеплоить саму программу и получить сам адресс программы, то вам надо ввести данную функцию:

anchor deploy

Также надо не забыть ввести данную функцию, которая запускает тестовый валидатор, если вы деплоите программу на локальной сети:

solana-test-validator

Не забывайте что нужно иметь около 3 соланы на кошельке если вы хотите задеплоить программу на любой сети.