Навчальні проєкти — це найшвидший шлях від розуміння концепцій Solana до робочого коду на Devnet. Кожен проєкт нижче вирішує одну конкретне завдання, має чіткі передумови, вказані версії та середовище виконання. Усі приклади орієнтовані на Devnet і не призначені для production без додаткового аудиту безпеки.
Спільне середовище для всіх проєктів:
- Solana CLI: 1.18.26
- Anchor: 0.30.1
- Rust: 1.77.0+
- Node.js: 20.x
- Кластер: Devnet (
solana config set --url devnet)
Як отримати тестові SOL на Devnet
Навіщо тестові SOL
Будь-яка транзакція на Solana потребує оплати комісії (compute budget + базова fee). Навіть розгортання порожньої програми на Devnet коштує близько 0.5–2 SOL. Тестові SOL не мають реальної вартості й отримуються безкоштовно через кран (faucet).
Через Solana CLI
Найпростіший спосіб для локальної розробки:
solana airdrop 2
Очікуваний результат: на балансі поточного ключа з'являється 2 SOL. Перевірити:
solana balance
Обмеження: кран CLI дозволяє отримати до 2 SOL за один запит і до 5 SOL на один ключ загалом. Якщо ліміт вичерпано, CLI поверне відповідне повідомлення.
Через TypeScript SDK
Корисно, коли SOL потрібен програмно — наприклад, у тестових сценаріях або у скриптах налаштування:
import { Connection, Keypair, LAMPORTS_PER_SOL } from "@solana/web3.js";
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const keypair = Keypair.generate();
const signature = await connection.requestAirdrop(keypair.publicKey, 2 * LAMPORTS_PER_SOL);
await connection.confirmTransaction(signature, "confirmed");
const balance = await connection.getBalance(keypair.publicKey);
console.log("Balance:", balance / LAMPORTS_PER_SOL, "SOL");
Очікуваний результат: виведе Balance: 2 SOL. Перевірте, що RPC-ендпоінт доступний і повертає відповідь.
Через Solana Faucet
Веб-кран доступний за адресою https://faucet.solana.com. Вставте публічний ключ, виберіть Devnet і натисніть кнопку. Цей спосіб корисний, коли CLI-кран досяг ліміту для вашого ключа.
Перевірка балансу
solana address
solana balance <ADDRESS>
Якщо баланс не оновився після airdrop, перевірте статус транзакції:
solana confirm -v <SIGNATURE>
Типові помилки
- «Airdrop limit reached» — створіть новий ключpair через
solana-keygen newабо використовуйте веб-кран. - Баланс залишається 0 після airdrop — перевірте, що ви підключені саме до Devnet:
solana config get. - RPC-таймаут — спробуйте інший Devnet RPC-ендпоінт або повторіть запит через кілька секунд.
Як структурувати повний Solana-проєкт
Монорепозиторій vs окремі репозиторії
Для навчальних проєктів монорепозиторій зручніший: усе в одному місці, спільні конфігурації, швидший перехід між on-chain і frontend кодом. Для production-команд з кількома програмами окремі репозиторії дають кращу ізоляцію.
Рекомендована структура монорепозиторію для навчального проєкту:
my-solana-project/
├── programs/
│ └── my-program/
│ ├── src/
│ │ └── lib.rs
│ ├── Cargo.toml
│ └── Xargo.toml
├── app/
│ ├── src/
│ │ ├── App.tsx
│ │ └── utils/
│ │ └── anchor.ts
│ ├── package.json
│ └── tsconfig.json
├── tests/
│ └── my-program.test.ts
├── Anchor.toml
├── Cargo.toml (workspace)
└── README.md
Розділення на модулі
У Rust-коді програми виділяйте логічні модулі через mod. Наприклад, якщо програма працює з токенами та голосуванням:
pub mod token_ops;
pub mod voting;
pub mod errors;
Кожен модуль — окремий файл у src/. Це зменшує когнітивне навантаження і полегшує пошук помилок.
Конфігурація
Anchor.toml — центральний конфіг. Для Devnet:
[features]
seeds = false
[programs.devnet]
my_program = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"
[registry]
url = "https://api.apr.dev"
[provider]
cluster = "Devnet"
wallet = "~/.config/solana/id.json"
Program ID у цьому файлі має збігатися з declare_id! у lib.rs. Після першого anchor build Anchor генерує реальний ключpair і оновлює ID — скопіюйте його у Anchor.toml.
Документація проєкту
Мінімум для навчального проєкту: README.md з описом що робить програма, як запустити локально, як деплоїти на Devnet, які інструкції доступні. Не описуйте теорію — лише практичні кроки.
Типові помилки
- Розбіжність Program ID між
Anchor.tomlіdeclare_id!— призводить до помилки розгортання. Після кожногоanchor buildперевіряйтеtarget/idl/my_program.jsonза актуальним ID. - Відсутній workspace Cargo.toml — Anchor очікує root-level
Cargo.tomlз[workspace]секцією. - Тести поза tests/ — Anchor запускає тести лише з директорії
tests/на рівні root.
Перший full-stack застосунок Solana
Архітектура застосунку
Найпростіший full-stack приклад: програма зберігає лічильник на ланцюжку, frontend відображає його значення і дозволяє збільшувати. Три шари: on-chain програма (Rust/Anchor), TypeScript-клієнт для взаємодії з програмою, frontend (React) для відображення.
Крок 1: on-chain програма
Ініціалізація:
anchor init counter-app --no-git
Замініть вміст programs/counter-app/src/lib.rs:
use anchor_lang::prelude::*;
declare_id!("YOUR_PROGRAM_ID_HERE");
#[account]
pub struct Counter {
pub count: u64,
pub authority: Pubkey,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = authority, space = 8 + 8 + 32)]
pub counter: Account<'info, Counter>,
#[account(mut)]
pub authority: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Increment<'info> {
#[account(mut, has_one = authority)]
pub counter: Account<'info, Counter>,
pub authority: Signer<'info>,
}
#[program]
pub mod counter_app {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
ctx.accounts.counter.count = 0;
ctx.accounts.counter.authority = ctx.accounts.authority.key();
Ok(())
}
pub fn increment(ctx: Context<Increment>) -> Result<()> {
ctx.accounts.counter.count += 1;
Ok(())
}
}
Очікуваний результат: anchor build завершується без помилок. Program ID генерується у target/deploy/.
Крок 2: TypeScript-клієнт
У tests/counter-app.test.ts:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { CounterApp } from "../target/types/counter_app";
import { Keypair, PublicKey, SystemProgram } from "@solana/web3.js";
describe("counter-app", () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.CounterApp as Program<CounterApp>;
const counter = Keypair.generate();
it("Initializes counter", async () => {
await program.methods.initialize().accounts({
counter: counter.publicKey,
authority: provider.wallet.publicKey,
systemProgram: SystemProgram.programId,
}).signers([counter]).rpc();
const data = await program.account.counter.fetch(counter.publicKey);
console.log("Count:", data.count.toNumber());
});
it("Increments counter", async () => {
await program.methods.increment().accounts({
counter: counter.publicKey,
authority: provider.wallet.publicKey,
}).rpc();
const data = await program.account.counter.fetch(counter.publicKey);
console.log("Count after increment:", data.count.toNumber());
});
});
Запуск: anchor test --skip-build. Очікуваний результат: обидва тести проходять, лічильник дорівнює 1.
Крок 3: frontend
У app/src/App.tsx — підключення до гаманця та виклик інструкцій. Ключовий момент: клієнтська частина використовує той самий IDL, що генерується при білді:
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { Program, AnchorProvider, BN } from "@coral-xyz/anchor";
import idl from "../../target/idl/counter_app.json";
const programId = new PublicKey(idl.address);
function App() {
const { connection } = useConnection();
const wallet = useWallet();
const getProvider = () => {
if (!wallet.publicKey) return null;
return new AnchorProvider(connection, wallet, {});
};
const increment = async () => {
const provider = getProvider();
if (!provider) return;
const program = new Program(idl, programId, provider);
// виклик increment з переданим counter publicKey
};
}
Крок 4: тестування
Спочатку запустіть тести локально (anchor test з локальним валідатором), потім на Devnet (див. розділ про тестування на Devnet).
Типові помилки
- «Account does not exist» — ви намагаєтеся прочитати лічильник до виклику
initialize. - «Constraint violated: has_one authority» — гаманець, що підписує
increment, відрізняється від того, що створив лічильник. - IDL не знайдено у frontend — перевірте шлях до
target/idl/відносно файлу компонента.
Як створити простий токен-контракт на Anchor
Що робитиме контракт
Цей навчальний проєкт — не новий токен-стандарт, а Anchor-програма-обгортка, яка через CPI викликає SPL Token Program для створення mint, карбування та передачі токенів. Це практичний приклад роботи з CPI, але без глибокого занурення в теорію (якщо вам потрібна теорія PDA та CPI — зверніться до відповідного розділу).
Структура програми
Залежності у Cargo.toml програми:
anchor-lang = "0.30.1"
anchor-spl = "0.30.1"
Реалізація create_token
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, Mint, TokenAccount};
declare_id!("YOUR_PROGRAM_ID");
#[derive(Accounts)]
pub struct CreateToken<'info> {
#[account(init, payer = authority, mint::decimals = 9, mint::authority = authority)]
pub mint: Account<'info, Mint>,
#[account(mut)]
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
pub rent: Sysvar<'info, Rent>,
}
pub fn create_token(ctx: Context<CreateToken>) -> Result<()> {
// Mint вже ініціалізано через anchor-spl макроси
Ok(())
}
Очікуваний результат: після виклику створюється новий mint-акаунт з 9 знаками після коми, авторитет карбування — викликач.
Реалізація mint та transfer
#[derive(Accounts)]
pub struct MintTokens<'info> {
#[account(mut)]
pub mint: Account<'info, Mint>,
#[account(mut, token::authority = authority)]
pub token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub authority: Signer<'info>,
pub token_program: Program<'info, Token>,
}
pub fn mint_tokens(ctx: Context<MintTokens>, amount: u64) -> Result<()> {
let cpi_ctx = CpiContext::new(
ctx.accounts.token_program.to_account_info(),
token::MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.token_account.to_account_info(),
authority: ctx.accounts.authority.to_account_info(),
},
);
token::mint_to(cpi_ctx, amount)?;
Ok(())
}
Transfer реалізується аналогічно через token::transfer з відповідним CpiContext.
Тестування контракту
У тесті створіть асоційований токен-акаунт (ATA) для отримувача перед карбуванням:
import { getAssociatedTokenAddress, getAccount } from "@solana/spl-token";
const ata = await getAssociatedTokenAddress(mint, provider.wallet.publicKey);
// переконайтеся, що ATA існує перед mint
const tokenAccount = await getAccount(provider.connection, ata);
Типові помилки
- «MintMismatch» — переданий mint не збігається з тим, що очікує програма.
- «OwnerMismatch for TokenAccount» — ATA належить іншому власнику.
- Забули додати token_program у структуру акаунтів — Anchor не зможе знайти SPL Token Program для CPI.
Як побудувати escrow-програму на Anchor
Що таке escrow
Escrow — програма-посередник, яка утримує активи обох сторін до виконання умов обміну. Класичний приклад: користувач A депонує токен X, користувач B депонує токен Y, програма атомарно обмінює їх. Якщо одна зі сторін не виконає свою частину — інша може скасувати угоду.
Структура escrow
#[account]
pub struct EscrowState {
pub maker: Pubkey,
pub taker: Pubkey,
pub mint_a: Pubkey,
pub mint_b: Pubkey,
pub amount_a: u64,
pub amount_b: u64,
pub bump: u8,
}
PDA для escrow-акаунта виводиться з seeds [b"escrow", maker.key().as_ref()].
Instruction: create escrow
Maker створює escrow-акаунт і переказує свої токени на програмний токен-акаунт (vault). Ключовий момент: vault — це PDA, який є власником токенів, а не програма безпосередньо.
#[derive(Accounts)]
pub struct CreateEscrow<'info> {
#[account(init, payer = maker, space = 8 + 32 + 32 + 32 + 32 + 8 + 8 + 1, seeds = [b"escrow", maker.key().as_ref()], bump)]
pub escrow: Account<'info, EscrowState>,
#[account(mut)]
pub maker: Signer<'info>,
#[account(mut)]
pub maker_token: Account<'info, TokenAccount>,
#[account(init, payer = maker, token::mint = mint_a, token::authority = escrow)]
pub vault: Account<'info, TokenAccount>,
pub mint_a: Account<'info, Mint>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
У тілі інструкції — CPI-виклик token::transfer від maker до vault.
Instruction: take escrow
Taker переказує свої токени maker-у і забирає токени з vault. Обидві передачі виконуються в одній транзакції — це гарантує атомарність.
Instruction: cancel escrow
Maker повертає свої токени з vault. Обмеження: лише maker може скасувати, і лише якщо escrow ще не взято.
Типові помилки
- «Vault has no tokens» — ви викликали
takeабоcancelбез попередньогоcreate. - «Seeds constraint violated» — неправильний порядок або вміст seeds при виведенні PDA.
- «Insufficient funds» — taker має менше токенів, ніж вказано в
amount_b.
Як створити систему голосування на Solana
Концепція голосування
Проста система: будь-хто може створити пропозицію (proposal) з описом, інші голосують «за» або «проти». Кожен голосувальник — один голос. Стан зберігається в двох типах акаунтів: пропозиція та запис голосу.
Структура даних
#[account]
pub struct Proposal {
pub author: Pubkey,
pub description: String,
pub yes_votes: u32,
pub no_votes: u32,
pub bump: u8,
}
#[account]
pub struct VoteRecord {
pub voter: Pubkey,
pub proposal: Pubkey,
pub vote: bool, // true = за, false = проти
pub bump: u8,
}
PDA для пропозиції: [b"proposal", author.key().as_ref(), proposal_id.to_le_bytes().as_ref()]. PDA для голосу: [b"vote", voter.key().as_ref(), proposal.key().as_ref()].
Instruction: create_proposal
pub fn create_proposal(ctx: Context<CreateProposal>, description: String, proposal_id: u64) -> Result<()> {
let proposal = &mut ctx.accounts.proposal;
proposal.author = ctx.accounts.author.key();
proposal.description = description;
proposal.yes_votes = 0;
proposal.no_votes = 0;
proposal.bump = ctx.bumps.proposal;
Ok(())
}
Обмеження description: у навчальному проєкті обмежте довжину рядка (наприклад, 256 байт) через перевірку в тілі функції, щоб уникнути перевищення ліміту розміру акаунта.
Instruction: vote
Перевіряє, що голосувальник ще не голосував за цю пропозицію (через init для VoteRecord — якщо акаунт вже існує, транзакція впаде). Збільшує відповідний лічильник у пропозиції.
Instruction: close_proposal
Дозволяє автору закрити пропозицію та повернути rent. Перевіряє has_one = author.
Типові помилки
- «Account already in use» — голосувальник намагається проголосувати двічі. Це очікувана поведінка завдяки
init. - Перевищення розміру акаунта — якщо
descriptionзанадто довгий, збільштеspaceуinitабо додайте перевірку довжини. - «Constraint violated: has_one author» — хтось інший намагається закрити пропозицію.
Як реалізувати прості платежі через програму
Концепція платежів
Програма-посередник для платежів SOL: відправник переказує SOL на тимчасовий акаунт (escrow), отримувач підтверджує виконання послуги, програма розблокує кошти. Це навчальний приклад, не готовий платіжний шлюз.
Структура програми
#[account]
pub struct Payment {
pub sender: Pubkey,
pub receiver: Pubkey,
pub amount: u64,
pub created_at: i64,
pub completed: bool,
pub bump: u8,
}
Instruction: create_payment
#[derive(Accounts)]
pub struct CreatePayment<'info> {
#[account(init, payer = sender, space = 8 + 32 + 32 + 8 + 8 + 1 + 1, seeds = [b"payment", sender.key().as_ref(), payment_id.to_le_bytes().as_ref()], bump)]
pub payment: Account<'info, Payment>,
#[account(mut)]
pub sender: Signer<'info>,
/// CHECK: receiver — це просто Pubkey, не потрібно десеріалізувати
pub receiver: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
pub fn create_payment(ctx: Context<CreatePayment>, amount: u64, payment_id: u64) -> Result<()> {
let payment = &mut ctx.accounts.payment;
payment.sender = ctx.accounts.sender.key();
payment.receiver = ctx.accounts.receiver.key();
payment.amount = amount;
payment.created_at = Clock::get()?.unix_timestamp;
payment.completed = false;
payment.bump = ctx.bumps.payment;
// Переказ SOL на PDA-акаунт payment
let transfer = anchor_lang::system_program::transfer(
CpiContext::new(
ctx.accounts.system_program.to_account_info(),
anchor_lang::system_program::Transfer {
from: ctx.accounts.sender.to_account_info(),
to: ctx.accounts.payment.to_account_info(),
},
),
amount,
);
transfer?;
Ok(())
}
Instruction: complete_payment
Перевіряє, що викликач — receiver, що платіж не завершений, і переказує SOL з PDA на акаунт receiver. Для цього PDA-акаунт має бути mut, а переказ виконується через system_program::transfer з PDA як signer (через seeds = [...] у CpiContext::new_with_signer).
Тестування платежів
У тесті створіть дві keypair: sender та receiver. Перевірте баланси до і після create_payment та complete_payment. Різниця має дорівнювати amount плюс комісії транзакцій.
Типові помилки
- «Insufficient lamports» — sender має недостатньо SOL для платежу плюс комісії.
- «Signature verification failed» — неправильні seeds при підписанні від імені PDA у
complete_payment. - «Payment already completed» — спроба завершити вже завершений платіж. Додайте перевірку
!payment.completed.
Як створити Telegram-бот для Solana
Архітектура бота
Telegram-бот — це Node.js процес, який слухає команди через Telegram Bot API і взаємодіє з Solana через RPC. Для навчального проєкту бот підтримує дві команди: баланс гаманця та створення транзакції.
Передумови: Node.js 20.x, встановлений пакет node-telegram-bot-api, @solana/web3.js, токен бота від BotFather.
Налаштування бота
import TelegramBot from "node-telegram-bot-api";
import { Connection, Keypair, LAMPORTS_PER_SOL, PublicKey } from "@solana/web3.js";
const token = process.env.TELEGRAM_BOT_TOKEN;
const bot = new TelegramBot(token, { polling: true });
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
Змінні середовища зберігайте у .env файлі (не комітьте його у репозиторій).
Команди бота
/balance <PUBLIC_KEY>— повертає баланс у SOL на Devnet./airdrop <PUBLIC_KEY>— запитує тестові SOL через кран.
Реалізація команд
bot.onText(/\/balance (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const publicKeyStr = match[1];
try {
const publicKey = new PublicKey(publicKeyStr);
const balance = await connection.getBalance(publicKey);
bot.sendMessage(chatId, `Баланс: ${balance / LAMPORTS_PER_SOL} SOL`);
} catch {
bot.sendMessage(chatId, "Невірний публічний ключ.");
}
});
bot.onText(/\/airdrop (.+)/, async (msg, match) => {
const chatId = msg.chat.id;
const publicKeyStr = match[1];
try {
const publicKey = new PublicKey(publicKeyStr);
const sig = await connection.requestAirdrop(publicKey, 2 * LAMPORTS_PER_SOL);
await connection.confirmTransaction(sig, "confirmed");
bot.sendMessage(chatId, `Airdrop успішний. Транзакція: ${sig}`);
} catch (e) {
bot.sendMessage(chatId, `Помилка: ${e.message}`);
}
});
Очікуваний результат: бот відповідає на команди в Telegram, повертаючи реальні дані з Devnet.
Типові помилки
- Бот не відповідає — перевірте, що
polling: trueі токен коректний. Також перевірте, що процес не впав через неопрацьований виняток — додайтеprocess.on('uncaughtException', ...). - RPC-таймаут у боті — Devnet RPC може бути повільним. Додайте таймаут і повторні спроби для
getBalance. - Зберігання приватних ключів у коді — ніколи не робіть цього. Якщо боту потрібно підписувати транзакції, використовуйте змінні середовища або окремий keypair-файл з обмеженими правами доступу.
Як поєднати frontend з on-chain програмою
Загальна архітектура
Frontend (React/Vite) підключається до Solana через три шари: гаманець (Phantom/Solflare) для підпису транзакцій, @solana/web3.js для зв'язку з RPC, @coral-xyz/anchor для типізованого виклику інструкцій програми. IDL-файл — це міст між Rust-програмою та TypeScript-клієнтом.
Налаштування клієнта
import { useConnection, useWallet } from "@solana/wallet-adapter-react";
import { WalletMultiButton } from "@solana/wallet-adapter-react-ui";
import { Program, AnchorProvider, Idl } from "@coral-xyz/anchor";
import idl from "../../target/idl/my_program.json";
import { PublicKey } from "@solana/web3.js";
const programId = new PublicKey((idl as Idl).address);
Обгорніть додаток у WalletProvider і ConnectionProvider на рівні кореневого компонента.
Виклик instructions з UI
const handleAction = async () => {
if (!wallet.publicKey || !wallet.signTransaction) return;
const provider = new AnchorProvider(connection, wallet as any, {});
const program = new Program(idl as Idl, programId, provider);
try {
const tx = await program.methods.myInstruction(param1, param2)
.accounts({
myAccount: accountPubkey,
authority: wallet.publicKey,
// інші акаунти
})
.rpc();
console.log("Transaction:", tx);
} catch (err) {
console.error("Помилка:", err);
}
};
Очікуваний результат: при натисканні кнопки гаманець відкривається для підпису, транзакція відправляється на Devnet.
Читання стану програми
const data = await program.account.myAccount.fetch(accountPubkey);
console.log("State:", data);
Для реактивного оновлення UI використовуйте useEffect з інтервалом або підписку на зміни акаунта через connection.onAccountChange.
Типові помилки
- «Wallet not connected» — виклик
.rpc()без підключеного гаманця. Завжди перевіряйтеwallet.publicKeyперед викликом. - «Account not found» — акаунт ще не ініціалізовано. Додайте перевірку існування акаунта перед читанням.
- IDL-несумісність — після зміни програми та перебілду оновіть IDL у frontend (іноді браузер кешує старий файл).
Як тестувати навчальний проєкт на Devnet
Підготовка тестового середовища
Переконайтеся, що CLI налаштований на Devnet і гаманець має SOL:
solana config set --url devnet
solana airdrop 2
У Anchor.toml переконайтеся, що cluster = "Devnet".
Написання тестів для Devnet
Тести Anchor за замовчуванням використовують локальний валідатор. Для Devnet створіть окремий конфіг:
// tests/devnet.test.ts
import * as anchor from "@coral-xyz/anchor";
const DEVNET_RPC = "https://api.devnet.solana.com";
const connection = new anchor.web3.Connection(DEVNET_RPC, "confirmed");
const wallet = anchor.AnchorProvider.local().wallet;
const provider = new anchor.AnchorProvider(connection, wallet, {
commitment: "confirmed",
skipPreflight: false,
});
anchor.setProvider(provider);
Тестовий сценарій
Повний цикл: ініціалізація → виконання інструкцій → читання стану → перевірка значень. Кожен крок — окремий it блок. Не залежайте від порядку виконання — кожен тест має бути самодостатнім або явно вказувати залежність.
Автоматизація
Додайте скрипт у package.json:
"test:devnet": "anchor test --skip-build -- --grep 'Devnet'"
Маркуйте Devnet-тести окремим describe-блоком, щоб можна було запускати їх ізольовано.
Типові помилки
- Тести падають з таймаутом — Devnet повільніший за локальний валідатор. Збільште таймаут у
jest.configабоmocha(наприклад, до 30 секунд). - «Blockhash not found» — RPC-ендпоінт повертає застарілий blockhash. Додайте
preflightCommitment: "confirmed"у провайдер. - Витрата тестових SOL — кожен тест на Devnet коштує реальні (тестові) SOL. Мінімізуйте кількість транзакцій у тестах.
Як деплоїти навчальний проєкт на Devnet
Структура повного проєкту
Перед деплоєм переконайтеся, що проєкт зібраний локально без помилок: anchor build. Перевірте, що target/deploy/my_program.so існує.
розгортання on-chain програми
anchor deploy --provider.cluster devnet
Очікуваний результат: програма розміщена на Devnet, у терміналі з'являється Program ID. Скопіюйте цей ID у Anchor.toml (якщо ще не оновлено) та у declare_id! у lib.rs, потім перезберіть і перездеплойте, якщо ID змінився.
Альтернативно через CLI:
solana program deploy target/deploy/my_program.so --program-id target/deploy/my_program-keypair.json
розгортання frontend
Для навчального проєкту достатньо Vite-білду:
cd app
npm run build
Результат — статичні файли у app/dist/. Розмістіть їх на будь-якому статичному хостингу (Vercel, Netlify, GitHub Pages). Налаштуйте змінні середовища для продакшн-RPC.
Налаштування змінних середовища
У frontend створіть .env.production:
VITE_SOLANA_RPC_URL=https://api.devnet.solana.com
VITE_PROGRAM_ID=ваш_реальний_program_id
У коді читайте через import.meta.env.VITE_PROGRAM_ID. Ніколи не вставляйте ключі безпосередньо у компоненти.
Перевірка розгортання
solana program show <PROGRAM_ID>
Перевірте, що програма відображається з правильним автором (вашим гаманцем) і розміром. Відкрийте frontend у браузері та спробуйте виконати базову інструкцію.
Типові помилки
- «Insufficient funds for deploy» — програма занадто велика для залишку SOL. Отримайте додаткові тестові SOL.
- Program ID не збігається — після першого розгортання ключpair фіксується. Якщо ви видалили
target/deploy/, програма отримає новий ID. Зберігайте keypair-файл. - Frontend підключається до Mainnet — перевірте, що
VITE_SOLANA_RPC_URLвказує на Devnet, а не наhttps://api.mainnet-beta.solana.com.
Наступний крок
Після завершення навчальних проєктів ви матимете робочий код на Devnet, розуміння повного циклу розробки та готову ментальну модель Solana-архітектури. Наступний логічний крок — поглиблені теми: безпека програм, індексація стану, production-діагностика та оптимізація compute budget. Ці теми вимагають окремого розгляду, оскільки помилки в них коштують реальні кошти на Mainnet.