Frontend-застосунок взаємодіє з on-chain програмою на Solana через три рівні: користувацький інтерфейс формує запит, TypeScript-клієнт перетворює його на транзакцію, а RPC-вузол передає її в мережу. Ця інструкція проведе вас від IDL-файлу програми до робочого UI, який відправляє транзакції та читає стан на Devnet.

Версії для прикладів у «Як поєднати frontend з on-chain програмою» перевірено 2 серпня 2026 року. Стабільна гілка Anchor v1 має релізи 1.0.x і орієнтується на Solana 3.x; Anchor v2 у документації позначений як alpha. Приклади для Anchor 0.29–0.32 залишаються лише відтворюваними прикладами для зафіксованого legacy-середовища: їх не слід переносити в новий проєкт без міграції залежностей і повторного тестування. Клієнт @anchor-lang/core сумісний із legacy @solana/web3.js v1, а не з v2.

Передумови:

  • Розгорнута on-chain програма на Devnet (наприклад, лічильник на Anchor)
  • IDL-файл цієї програми (зазвичай у директорії target/idl/)
  • Node.js 18+, npm або pnpm
  • Гаманець із SOL на Devnet (отримати через faucet)

Середовище: Devnet-кластер Solana, Anchor 0.29.x, @solana/web3.js 1.87+, @coral-xyz/anchor 0.29.x

Очікуваний результат: frontend-застосунок, який відображає стан on-chain акаунта та відправляє транзакції через гаманець користувача.

Загальна архітектура

Frontend → TypeScript-клієнт → RPC → On-chain програма

Архітектура з'єднання frontend з on-chain програмою складається з чотирьох послідовних шарів:

  1. Frontend (React, Vue, Svelte) — відображає дані та реагує на дії користувача.
  2. TypeScript-клієнт (згенерований з IDL) — формує правильну структуру транзакції.
  3. RPC-вузол — приймає серіалізовану транзакцію та транслює її в мережу.
  4. On-chain програма — виконує бізнес-логіку та змінює стан.

Ключовий момент: frontend ніколи не формує транзакцію вручну. Він делегує це TypeScript-клієнту, який знає точну структуру інструкцій, акаунтів та даних програми.

IDL як міст між програмою та frontend

IDL (Interface Definition Language) — це JSON-файл, який Anchor генерує під час компіляції програми. Він містить:

  • адресу програми (programId)
  • структури акаунтів та їхні поля
  • сигнатури інструкцій із переліком акаунтів та аргументів
  • типи помилок

IDL виконує ту саму роль, що й OpenAPI-специфікація для REST API: дає клієнту повну інформацію про те, як правильно звертатися до програми, без необхідності читати Rust-код.

Налаштування клієнта

Генерація TypeScript-клієнта з IDL

Перший крок — перенести IDL у frontend-проєкт та створити з нього типізований клієнт.

Створіть директорію для IDL у вашому frontend-проєкті:

src/idl/

Скопіюйте файл IDL (наприклад, counter.json) з target/idl/ вашого Anchor-проєкту до цієї директорії.

Встановіть залежності:

npm install @coral-xyz/anchor @solana/web3.js

Імпортуйте IDL та створіть клієнт програми:

import { AnchorProvider, Program, Idl } from "@coral-xyz/anchor";
import { Connection, PublicKey } from "@solana/web3.js";
import idl from "../idl/counter.json";

const PROGRAM_ID = new PublicKey(idl.metadata.address);

const connection = new Connection(
"https://api.devnet.solana.com",
"confirmed"
);

const program = new Program(idl as Idl, PROGRAM_ID, provider);

Тут Program автоматично створює типізовані методи для кожної інструкції з IDL та об'єкти для читання акаунтів.

Ініціалізація AnchorProvider

AnchorProvider об'єднує з'єднання, гаманець та параметри транзакції. Для навчального проєкту на Devnet достатньо базової ініціалізації:

import { AnchorProvider } from "@coral-xyz/anchor";

// Приклад із використанням @solana/wallet-adapter-react
const wallet = useWallet();

const provider = new AnchorProvider(connection, wallet, {
commitment: "confirmed",
preflightCommitment: "confirmed",
});

Очікуваний результат: об'єкт provider готовий до передачі в Program. Якщо гаманець не підключено, provider.wallet буде у стані null, і спроба надіслати транзакцію завершиться помилкою — це коректна поведінка, яку варто обробити в UI.

Виклик instructions з UI

Формування транзакції через клієнт

Розглянемо виклик інструкції increment програми-лічильника. Припустимо, IDL описує її так:

{
  "name": "increment",
  "accounts": [
    { "name": "counter", "isMut": true, "isSigner": false }
  ],
  "args": []
}

TypeScript-клієнт створює метод program.methods.increment(), який повертає об'єкт-будівник транзакції. Вам залишається передати акаунти:

async function handleIncrement() {
  const [counterPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("counter")],
    PROGRAM_ID
  );

  try {
    const tx = await program.methods
      .increment()
      .accounts({
        counter: counterPda,
      })
      .rpc();

    console.log("Транзакція відправлена:", tx);
  } catch (err) {
    console.error("Помилка транзакції:", err);
  }
}

Зверніть увагу: .rpc() формує, підписує та відправляє транзакцію одним викликом. Це зручно для навчальних проєктів. У production-рішеннях частіше використовують .transaction() для ручного контролю над підписанням та відправкою.

Підписання гаманцем та відправка

Коли ви викликаєте .rpc(), AnchorProvider автоматично:

  1. Додає інструкцію до нової транзакції.
  2. Додає compute budget (за замовчуванням).
  3. Передає транзакцію гаманцю на підписання.
  4. Відправляє підписану транзакцію через RPC.
  5. Чекає підтвердження з вказаним commitment.

Якщо гаманець вимагає підтвердження від користувача (як Phantom або Solflare), з'явиться спливаюче вікно. Поки користувач не натисне «Approve» або «Reject», виклик .rpc() залишається в очікуванні.

Перевірка: після успішного виклику в консолі з'явиться рядок із сигнатурою транзакції (base58, близько 87 символів). Перевірити її статус можна на Solana Explorer, обравши кластер Devnet.

Читання стану програми

Отримання даних через program.account.*

Anchor-клієнт генерує об'єкти для кожного типу акаунта з IDL. Якщо ваш IDL містить структуру Counter з полями authority та count, клієнт створить program.account.counter:

async function fetchCounter() {
  const [counterPda] = PublicKey.findProgramAddressSync(
    [Buffer.from("counter")],
    PROGRAM_ID
  );

  try {
    const account = await program.account.counter.fetch(counterPda);
    return account;
  } catch {
    // Акаунт ще не ініціалізовано
    return null;
  }
}

Метод .fetch() повертає десеріалізований об'єкт із типізованими полями. Якщо акаунт не існує, він викидає помилку — саме тому обгортка в try/catch є обов'язковою.

Для отримання всіх акаунтів певного типу використовуйте .all():

const allCounters = await program.account.counter.all();

Це корисно для адмін-панелей, але в навчальному проєкті з одним лічильником достатньо .fetch().

Оновлення UI при зміні стану

Найпростіший спосіб тримати UI актуальним — періодичне опитування (polling). Для навчального проєкту на Devnet це прийнятний підхід:

import { useEffect, useState } from "react";

function CounterDisplay() {
  const [count, setCount] = useState<number | null>(null);

  useEffect(() => {
    const interval = setInterval(async () => {
      const data = await fetchCounter();
      setCount(data ? data.count : null);
    }, 2000);

    return () => clearInterval(interval);
  }, []);

  return <p>Лічильник: {count ?? "не ініціалізовано"}</p>;
}

Обмеження: polling створює навантаження на RPC і не гарантує миттєвого оновлення. У production-рішеннях замість нього використовують WebSocket-підписки через program.account.counter.subscribe() або зовнішні індексатори. Це окрема тема, яка виходить за межі поточного матеріалу.

Типові помилки

IDL не знайдено або застарів

Симптом: Error: Account does not exist при зверненні до існуючого акаунта, або TypeError: Cannot read properties of undefined при виклику методу інструкції.

Причина: IDL у frontend-проєкті не збігається з тим, що реально розгорнуто на Devnet. Це трапляється, коли ви змінили структуру акаунта або сигнатуру інструкції в Rust-коді, перезібрали програму, але забули оновити IDL-файл у frontend.

Рішення: після кожної перезбірки програми (anchor build) копіюйте оновлений IDL з target/idl/ у ваш frontend-проєкт. Перевірте, що поле metadata.address у IDL збігається з programId, який ви використовуєте в клієнті.

Невірний порядок акаунтів у клієнті

Симптом: транзакція формується без помилок, але при відправці повертає Transaction signature verification failure або InstructionFallbackNotFound.

Причина: Anchor порівнює акаунти, передані в транзакції, з тими, що вказані в IDL, за іменами — а не за порядком. Однак якщо ви передали акаунти у вигляді масиву замість об'єкта з іменованими ключами, порядок стає критичним.

Неправильно:

.accounts([counterPda]) // масив — порядок має збігатися з IDL

Правильно:

.accounts({ counter: counterPda }) // об'єкт — Anchor зіставляє за іменами

Рішення: завжди використовуйте об'єкт із іменованими ключами при виклику .accounts(). Це усуває цілий клас помилок, пов'язаних із порядком, і робить код читабельнішим.

Джерела