Правильна структура проєкту — це не естетика, а інструмент, який визначає, наскільки швидко ви зрозумієте власний код через місяць і чи зможете колега запустити його без ваших пояснень. У Solana-розробці це особливо важливо, бо проєкт зазвичай містить смарт-контракти мовою Rust, клієнтську частину на TypeScript або JavaScript та скрипти для взаємодії з блокчейном. Кожна з цих частин має власний життєвий цикл, залежності та інструменти збірки.
Нижче наведена перевірена модель структури проєкту для навчальних цілей на Devnet. Вона базується на фреймворку Anchor і працює в середовищі Linux або macOS з встановленими Node.js, Rust та Solana CLI. Конкретні версії залежностей перевірте в офіційній документації Anchor та Solana на момент початку роботи, оскільки вони оновлюються.
Монорепозиторій vs окремі репозиторії
Перше рішення, яке треба прийняти: усі компоненти в одному репозиторії (монорепо) чи в окремих. Для навчального проєкту та більшості MVP на Solana монорепо є раціональним вибором.
Коли обрати монорепо
Монорепо підходить, якщо смарт-контракти та клієнтська частина розвиваються паралельно, ви часто змінюєте типи даних одночасно в обох частинах і команда складається з одного–трьох розробників. Головна перевага — синхронізація змін. Коли ви додаєте нове поле в структуру акаунта в Rust, ви одразу оновлюєте відповідний TypeScript-тип у клієнті, і обидві зміни потрапляють в один коміт.
Окремі репозиторії мають сенс, коли смарт-контракт уже стабільний і працює на Mainnet, а клієнтську частину розробляє окрема команда з власним CI/CD. Для поточного етапу — навчальний проєкт на Devnet — це ускладнення без практичної користі.
Структура каталогів монорепо
Базова структура монорепо для Anchor-проєкту виглядає так:
- programs/ — вихідний код смарт-контрактів мовою Rust
- app/ — клієнтська частина (frontend або CLI)
- tests/ інтеграційні тести смарт-контрактів
- scripts/ — утилітарні скрипти (розгортання, ініціалізація)
- shared/ — спільні типи та константи
- Anchor.toml — конфігурація проєкту Anchor
- package.json — залежності та скрипти Node.js
- tsconfig.json — конфігурація TypeScript
Ця структура не випадкова: вона відповідає тому, як Anchor очікує бачити файли, і водночас чітко розділяє зони відповідальності.
Розділення на модулі
programs/, app/, scripts/, tests/
programs/ містить підкаталоги для кожного смарт-контракту. Навіть якщо у вас один контракт, він лежить у власному підкаталозі, наприклад programs/my_program/. Всередині — стандартна структура Cargo-проєкту: Cargo.toml, src/lib.rs та інші модулі. Не розміщуйте в цьому каталозі нічого, що не компілюється як Solana-програма.
app/ — це місце для клієнтського коду. У навчальному проєкті це може бути простий Node.js-скрипт або мінімальний frontend. Головне правило: код у app/ не повинен імпортувати файли з programs/ безпосередньо. Взаємодія йде через згенерований IDL (Interface Definition Language), який Anchor створює після збірки контракту.
tests/ містить інтеграційні тести, які Anchor запускає командою anchor test. Ці тести працюють проти локального validator і перевіряють поведінку смарт-контрактів на рівні транзакцій. Вони відрізняються від модульних тестів Rust (які лежать усередині programs/) тим, що імітують реальну взаємодію клієнта з блокчейном.
scripts/ призначений для одноразових або рідкісних операцій: створення токена, розподіл початкових коштів на Devnet, міграція стану. Скрипти тут не є частиною збірки і не запускаються автоматично. Кожен скрипт — це окремий файл з очевидною назвою, наприклад create-mint.ts.
Спільні типи та утиліти (shared/)
Каталог shared/ вирішує конкретну проблему: одні й ті ж константи (наприклад, адреса програми, розміри структур) потрібні і в Rust, і в TypeScript. Оскільки ці мови не мають спільної системи типів, shared/ не містить коду, який імпортується обома мовами безпосередньо. Натомість тут зберігається:
- shared/constants.ts — адреси програм, ідентифікатори пулів, ліміти, які використовує клієнтська частина
- shared/notes.md — текстовий опис полів структур, щоб при зміні контракту ви могли вручну оновити TypeScript-типи
Це навмисне спрощення для навчального проєкту. У production-рішеннях використовують генератори типів (наприклад, @coral-xyz/anchor-ts або кастомні скрипти), які автоматично створюють TypeScript-типи з IDL. Але для початкового етапу ручна синхронізація через shared/ дає краще розуміння того, які дані передаються між контрактом і клієнтом.
Конфігурація
Anchor.toml — конфігурація Anchor
Anchor.toml — це центральний файл конфігурації, який Anchor читає при кожній команді. Мінімальний робочий варіант для Devnet:
[features]
seeds = false
[programs.devnet]
my_program = "YourProgramIDHere"
[registry]
url = "https://api.apr.dev"
Ключові моменти. Секція [programs.devnet] прив'язує ім'я програми до її адреси (Program ID) на конкретному кластері. Program ID генерується при першому розгортанні — саме цей рядок треба оновити після розгортання. Якщо ви працюєте на локальному validator, використовуйте секцію [programs.localnet] з тим самим Program ID.
Значення seeds = false вимикає відновлення тестових акаунтів між запусками тестів. Для навчального проєкту це прийнятно: кожен тест створює чистий стан. У production-тестуванні це значення змінюють на true і налаштовують каталог для збереження стану.
package.json — скрипти та залежності
package.json знаходиться в корені репозиторію і координує всі Node.js-операції. Типові скрипти, які варто визначити одразу:
- "build" —
anchor build, збирає смарт-контракти та генерує IDL - "test" —
anchor test, запускає локальний validator і інтеграційні тести - "test:devnet" — запускає тести проти Devnet (потребує налаштованого RPC-ендпоінту)
Залежності варто розділити на dependencies (потрібні в рантаймі — для app/) та devDependencies (потрібні лише для розробки та тестування — для tests/ і scripts/). До devDependencies належать @coral-xyz/anchor, @solana/web3.js та chai для тестів.
Документація проєкту
README: встановлення, запуск, тестування
README у корені репозиторію має відповідати на три питання в такому порядку:
- Передумови. Що має бути встановлено на машині (Node.js, Rust, Solana CLI, Anchor) і де це завантажити. Не варто переписувати інструкції встановлення — достатньо посилатися на офіційні джерела та вказати мінімальні версії, які ви перевірили.
- Запуск. Послідовність команд: клонування, встановлення залежностей (
npm install), збірка (npm run build), запуск тестів (npm test). Кожна команда — в окремому блоці коду. - Конфігурація. Що саме треба змінити перед першим запуском (Program ID у
Anchor.toml, RPC-ендпоінт, приватний ключ для Devnet, якщо скрипти його використовують).
Очікуваний результат: розробник, який клонує репозиторій на чисту машину з виконаними передумовами, має отримати проходження всіх тестів не більше ніж за п'ять хвилин без ваших додаткових пояснень.
Коментарі у коді та архітектурні нотатки
У смарт-контрактах (Rust) коментарі потрібні там, де логіка неочевидна з коду. Типовий випадок — обчислення PDA (Program Derived Address, похідна адреса програми, яка детерміновано генерується з seeds). Замість коментаря // обчислюємо PDA напишіть, які саме seeds використовуються і чому:
// PDA рахунку користувача: seeds = [b"user", user_pubkey.as_ref()]
// Вибрано саме ці seeds, щоб кожен користувач мав рівно один рахунок
// і його адресу можна обчислити офлайн, знаючи лише публічний ключ
Архітектурні рішення, які стосуються всього проєкту, фіксуйте у файлі shared/notes.md, а не розкидайте по коментарях. Туди записуйте: чому обрано таку схему акаунтів, які обмеження Solana вплинули на дизайн, які альтернативи розглядалися.
Типові помилки
Занадто глибока вкладеність каталогів
Початківці іноді створюють структуру на п'ять–шість рівнів вкладеності, намагаючись згрупувати все за абстрактними категоріями. У Solana-проєкті це створює дві проблеми. По-перше, шляхи до модулів у Rust стають довгими, що ускладнює mod та use декларації. По-друге, таку структуру важко навігувати в редакторі коду.
Правило: якщо каталог містить менше двох файлів і не планує рости — він, імовірно, не потрібен. Три рівні вкладеності від кореня до файлу з кодом — це зазвичай достатньо для навчального проєкту.
Залежності між модулями створюють цикл
Циклічні залежності виникають, коли app/ імпортує щось із scripts/, а scripts/ — із app/, або коли два смарт-контракти в programs/ посилаються один на одного через CPI (Cross-Program Invocation) у обидва боки. У Rust циклічні залежності на рівні крейтів взагалі не компілюються. У TypeScript вони технічно працюють, але створюють непередбачувану поведінку під час імпорту.
Як цього уникнути. Дотримуйтеся односпрямованого потоку залежностей:
- shared/ не залежить ні від чого
- programs/ залежить лише від стандартної бібліотеки Solana та Anchor
- tests/ залежить від
programs/(через IDL) таshared/ - app/ залежить від
programs/(через IDL) таshared/ - scripts/ залежить від
shared/та, за потреби, від IDL програми
Якщо ви виявили, що scripts/ потребує логіки з app/ — витягніть цю логіку в окремий файл у shared/ або перегляньте, чи справді скрипт виконує свою функцію. Скрипти мають бути простими операціями, а не клієнтською логікою.
Описана структура дає стабільну основу, яку можна масштабувати: додавати нові програми в programs/, нові скрипти в scripts/, нові тести в tests/ — без перебудови існуючих каталогів. Коли проєкт переросте навчальний етап і знадобляться production-налаштування (окремі конфігурації для різних кластерів, CI/CD пайплайни, моніторинг), ця структура дозволить додавати їх послідовно, а не переписувати з нуля.