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 програмою складається з чотирьох послідовних шарів:
- Frontend (React, Vue, Svelte) — відображає дані та реагує на дії користувача.
- TypeScript-клієнт (згенерований з IDL) — формує правильну структуру транзакції.
- RPC-вузол — приймає серіалізовану транзакцію та транслює її в мережу.
- 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 автоматично:
- Додає інструкцію до нової транзакції.
- Додає compute budget (за замовчуванням).
- Передає транзакцію гаманцю на підписання.
- Відправляє підписану транзакцію через RPC.
- Чекає підтвердження з вказаним 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(). Це усуває цілий клас помилок, пов'язаних із порядком, і робить код читабельнішим.